Compare commits
No commits in common. "53d43b570749f9072ea0662ffafd8b7db630b132" and "53611d80d6e5e90eac1c3c114c1e38fb3830a845" have entirely different histories.
53d43b5707
...
53611d80d6
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,22 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2023.8 (104)
|
|
||||||
Features/Improvements:
|
|
||||||
- Show search operators on Mastodon 4.2
|
|
||||||
- Enable composing local-only posts on Akkoma
|
|
||||||
- Update timestamps after refreshing notifications/timelines
|
|
||||||
- Improve list appearance in rich text posts
|
|
||||||
- Improve error message when uploading attachment to Pixelfed fails
|
|
||||||
- Compress uploaded videos to fit within instance limits
|
|
||||||
- iPad: Allow switching between split screen and full screen navigation
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix replies to posts with content warnings always showing confirmation dialog before closing
|
|
||||||
- Fix Live Text control reappearing when swiping between attachment gallery pages
|
|
||||||
- Fix avatars on certain notifications flickering when refreshing
|
|
||||||
- iPad: Fix delay on app launch before "My Profile" sidebar item appears
|
|
||||||
- macOS: Fix "New Post" window title appearing twice
|
|
||||||
|
|
||||||
## 2023.7 (103)
|
## 2023.7 (103)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Add support for iOS 17
|
- Add support for iOS 17
|
||||||
|
|
|
@ -72,12 +72,12 @@ class PostService: ObservableObject {
|
||||||
mediaIDs: uploadedAttachments,
|
mediaIDs: uploadedAttachments,
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoilerText: contentWarning,
|
spoilerText: contentWarning,
|
||||||
visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue,
|
visibility: draft.visibility,
|
||||||
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
||||||
pollOptions: draft.poll?.pollOptions.map(\.text),
|
pollOptions: draft.poll?.pollOptions.map(\.text),
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
pollMultiple: draft.poll?.multiple,
|
pollMultiple: draft.poll?.multiple,
|
||||||
localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil,
|
localOnly: mastodonController.instanceFeatures.localOnlyPosts ? draft.localOnly : nil,
|
||||||
idempotencyKey: draft.id.uuidString
|
idempotencyKey: draft.id.uuidString
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,11 +53,10 @@ class ToolbarController: ViewController {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
cwButton
|
cwButton
|
||||||
|
|
||||||
MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly)
|
MenuPicker(selection: $draft.visibility, options: visibilityOptions, buttonStyle: .iconOnly)
|
||||||
// the button has a bunch of extra space by default, but combined with what we add it's too much
|
// the button has a bunch of extra space by default, but combined with what we add it's too much
|
||||||
.padding(.horizontal, -8)
|
.padding(.horizontal, -8)
|
||||||
.disabled(draft.editedStatusID != nil)
|
.disabled(draft.editedStatusID != nil)
|
||||||
.disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly)
|
|
||||||
|
|
||||||
if composeController.mastodonController.instanceFeatures.localOnlyPosts {
|
if composeController.mastodonController.instanceFeatures.localOnlyPosts {
|
||||||
localOnlyPicker
|
localOnlyPicker
|
||||||
|
@ -119,20 +118,9 @@ class ToolbarController: ViewController {
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var visibilityBinding: Binding<Pachyderm.Visibility> {
|
|
||||||
// On instances that conflate visibliity and local only, we still show two separate controls but don't allow
|
|
||||||
// changing the visibility when local-only.
|
|
||||||
if draft.localOnly,
|
|
||||||
composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility {
|
|
||||||
return .constant(.public)
|
|
||||||
} else {
|
|
||||||
return $draft.visibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var visibilityOptions: [MenuPicker<Pachyderm.Visibility>.Option] {
|
private var visibilityOptions: [MenuPicker<Pachyderm.Visibility>.Option] {
|
||||||
let visibilities: [Pachyderm.Visibility]
|
let visibilities: [Pachyderm.Visibility]
|
||||||
if !composeController.mastodonController.instanceFeatures.composeDirectStatuses {
|
if !controller.parent.mastodonController.instanceFeatures.composeDirectStatuses {
|
||||||
visibilities = [.public, .unlisted, .private]
|
visibilities = [.public, .unlisted, .private]
|
||||||
} else {
|
} else {
|
||||||
visibilities = Pachyderm.Visibility.allCases
|
visibilities = Pachyderm.Visibility.allCases
|
||||||
|
|
|
@ -25,7 +25,6 @@ public class Draft: NSManagedObject, Identifiable {
|
||||||
@NSManaged public var contentWarningEnabled: Bool
|
@NSManaged public var contentWarningEnabled: Bool
|
||||||
@NSManaged public var editedStatusID: String?
|
@NSManaged public var editedStatusID: String?
|
||||||
@NSManaged public var id: UUID
|
@NSManaged public var id: UUID
|
||||||
@NSManaged public var initialContentWarning: String?
|
|
||||||
@NSManaged public var initialText: String
|
@NSManaged public var initialText: String
|
||||||
@NSManaged public var inReplyToID: String?
|
@NSManaged public var inReplyToID: String?
|
||||||
@NSManaged public var language: String? // ISO 639 language code
|
@NSManaged public var language: String? // ISO 639 language code
|
||||||
|
@ -66,7 +65,7 @@ public class Draft: NSManagedObject, Identifiable {
|
||||||
extension Draft {
|
extension Draft {
|
||||||
public var hasContent: Bool {
|
public var hasContent: Bool {
|
||||||
(!text.isEmpty && text != initialText) ||
|
(!text.isEmpty && text != initialText) ||
|
||||||
(contentWarningEnabled && !contentWarning.isEmpty && contentWarning != initialContentWarning) ||
|
(contentWarningEnabled && !contentWarning.isEmpty) ||
|
||||||
attachments.count > 0 ||
|
attachments.count > 0 ||
|
||||||
poll?.hasContent == true
|
poll?.hasContent == true
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,7 +216,7 @@ extension DraftAttachment {
|
||||||
options.isNetworkAccessAllowed = true
|
options.isNetworkAccessAllowed = true
|
||||||
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { exportSession, info in
|
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { exportSession, info in
|
||||||
if let exportSession {
|
if let exportSession {
|
||||||
Self.exportVideoData(session: exportSession, features: features, completion: completion)
|
Self.exportVideoData(session: exportSession, completion: completion)
|
||||||
} else if let error = info?[PHImageErrorKey] as? Error {
|
} else if let error = info?[PHImageErrorKey] as? Error {
|
||||||
completion(.failure(.videoExport(error)))
|
completion(.failure(.videoExport(error)))
|
||||||
} else {
|
} else {
|
||||||
|
@ -242,7 +242,7 @@ extension DraftAttachment {
|
||||||
completion(.failure(.noVideoExportSession))
|
completion(.failure(.noVideoExportSession))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Self.exportVideoData(session: session, features: features, completion: completion)
|
Self.exportVideoData(session: session, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
let fileData: Data
|
let fileData: Data
|
||||||
do {
|
do {
|
||||||
|
@ -300,12 +300,9 @@ extension DraftAttachment {
|
||||||
return (data, type)
|
return (data, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func exportVideoData(session: AVAssetExportSession, features: InstanceFeatures, completion: @escaping (Result<(Data, UTType), ExportError>) -> Void) {
|
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, UTType), ExportError>) -> 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")
|
||||||
if let configuration = features.mediaAttachmentsConfiguration {
|
|
||||||
session.fileLengthLimit = Int64(configuration.videoSizeLimit)
|
|
||||||
}
|
|
||||||
session.exportAsynchronously {
|
session.exportAsynchronously {
|
||||||
guard session.status == .completed else {
|
guard session.status == .completed else {
|
||||||
completion(.failure(.videoExport(session.error!)))
|
completion(.failure(.videoExport(session.error!)))
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22221.1" systemVersion="22G74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Draft" representedClassName="ComposeUI.Draft" syncable="YES">
|
<entity name="Draft" representedClassName="ComposeUI.Draft" syncable="YES">
|
||||||
<attribute name="accountID" attributeType="String"/>
|
<attribute name="accountID" attributeType="String"/>
|
||||||
<attribute name="contentWarning" attributeType="String" defaultValueString=""/>
|
<attribute name="contentWarning" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="contentWarningEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="contentWarningEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="editedStatusID" optional="YES" attributeType="String"/>
|
<attribute name="editedStatusID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="initialContentWarning" optional="YES" attributeType="String"/>
|
|
||||||
<attribute name="initialText" attributeType="String"/>
|
<attribute name="initialText" attributeType="String"/>
|
||||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||||
<attribute name="language" optional="YES" attributeType="String"/>
|
<attribute name="language" optional="YES" attributeType="String"/>
|
||||||
|
|
|
@ -88,7 +88,6 @@ public class DraftsPersistentContainer: NSPersistentContainer {
|
||||||
draft.text = text
|
draft.text = text
|
||||||
draft.initialText = text
|
draft.initialText = text
|
||||||
draft.contentWarning = contentWarning
|
draft.contentWarning = contentWarning
|
||||||
draft.initialContentWarning = contentWarning
|
|
||||||
draft.contentWarningEnabled = !contentWarning.isEmpty
|
draft.contentWarningEnabled = !contentWarning.isEmpty
|
||||||
draft.inReplyToID = inReplyToID
|
draft.inReplyToID = inReplyToID
|
||||||
draft.visibility = visibility
|
draft.visibility = visibility
|
||||||
|
@ -113,7 +112,6 @@ public class DraftsPersistentContainer: NSPersistentContainer {
|
||||||
draft.initialText = source.text
|
draft.initialText = source.text
|
||||||
draft.contentWarning = source.spoilerText
|
draft.contentWarning = source.spoilerText
|
||||||
draft.contentWarningEnabled = !source.spoilerText.isEmpty
|
draft.contentWarningEnabled = !source.spoilerText.isEmpty
|
||||||
draft.initialContentWarning = source.spoilerText
|
|
||||||
draft.inReplyToID = inReplyToID
|
draft.inReplyToID = inReplyToID
|
||||||
draft.visibility = visibility
|
draft.visibility = visibility
|
||||||
draft.localOnly = localOnly
|
draft.localOnly = localOnly
|
||||||
|
|
|
@ -21,28 +21,16 @@ public class InstanceFeatures: ObservableObject {
|
||||||
@Published public private(set) var charsReservedPerURL = 23
|
@Published public private(set) var charsReservedPerURL = 23
|
||||||
@Published public private(set) var maxPollOptionChars: Int?
|
@Published public private(set) var maxPollOptionChars: Int?
|
||||||
@Published public private(set) var maxPollOptionsCount: Int?
|
@Published public private(set) var maxPollOptionsCount: Int?
|
||||||
@Published public private(set) var mediaAttachmentsConfiguration: Instance.MediaAttachmentsConfiguration?
|
|
||||||
|
|
||||||
public var localOnlyPosts: Bool {
|
public var localOnlyPosts: Bool {
|
||||||
switch instanceType {
|
switch instanceType {
|
||||||
case .mastodon(.hometown(_), _), .mastodon(.glitch, _):
|
case .mastodon(.hometown(_), _), .mastodon(.glitch, _):
|
||||||
return true
|
return true
|
||||||
case .pleroma(.akkoma(_)):
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instance types that use a separate visibility to indicate local-only posts.
|
|
||||||
public var localOnlyPostsVisibility: Bool {
|
|
||||||
if case .pleroma(.akkoma(_)) = instanceType {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var mastodonAttachmentRestrictions: Bool {
|
public var mastodonAttachmentRestrictions: Bool {
|
||||||
instanceType.isMastodon
|
instanceType.isMastodon
|
||||||
}
|
}
|
||||||
|
@ -167,10 +155,6 @@ public class InstanceFeatures: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var searchOperators: Bool {
|
|
||||||
hasMastodonVersion(4, 2, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +211,6 @@ public class InstanceFeatures: ObservableObject {
|
||||||
maxPollOptionChars = pollsConfig.maxCharactersPerOption
|
maxPollOptionChars = pollsConfig.maxCharactersPerOption
|
||||||
maxPollOptionsCount = pollsConfig.maxOptions
|
maxPollOptionsCount = pollsConfig.maxOptions
|
||||||
}
|
}
|
||||||
mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
|
|
||||||
|
|
||||||
_featuresUpdated.send()
|
_featuresUpdated.send()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1500"
|
LastUpgradeVersion = "1400"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -113,7 +113,6 @@ public class Client {
|
||||||
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
|
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
|
||||||
urlRequest.httpMethod = request.method.name
|
urlRequest.httpMethod = request.method.name
|
||||||
urlRequest.httpBody = request.body.data
|
urlRequest.httpBody = request.body.data
|
||||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
||||||
for (name, value) in request.headers {
|
for (name, value) in request.headers {
|
||||||
urlRequest.setValue(value, forHTTPHeaderField: name)
|
urlRequest.setValue(value, forHTTPHeaderField: name)
|
||||||
}
|
}
|
||||||
|
@ -396,7 +395,7 @@ public class Client {
|
||||||
mediaIDs: [String]? = nil,
|
mediaIDs: [String]? = nil,
|
||||||
sensitive: Bool? = nil,
|
sensitive: Bool? = nil,
|
||||||
spoilerText: String? = nil,
|
spoilerText: String? = nil,
|
||||||
visibility: String? = nil,
|
visibility: Visibility? = nil,
|
||||||
language: String? = nil, // language supported by mastodon and akkoma
|
language: String? = nil, // language supported by mastodon and akkoma
|
||||||
pollOptions: [String]? = nil,
|
pollOptions: [String]? = nil,
|
||||||
pollExpiresIn: Int? = nil,
|
pollExpiresIn: Int? = nil,
|
||||||
|
@ -409,7 +408,7 @@ public class Client {
|
||||||
"in_reply_to_id" => inReplyTo,
|
"in_reply_to_id" => inReplyTo,
|
||||||
"sensitive" => sensitive,
|
"sensitive" => sensitive,
|
||||||
"spoiler_text" => spoilerText,
|
"spoiler_text" => spoilerText,
|
||||||
"visibility" => visibility,
|
"visibility" => visibility?.rawValue,
|
||||||
"language" => language,
|
"language" => language,
|
||||||
"poll[expires_in]" => pollExpiresIn,
|
"poll[expires_in]" => pollExpiresIn,
|
||||||
"poll[multiple]" => pollMultiple,
|
"poll[multiple]" => pollMultiple,
|
||||||
|
|
|
@ -11,20 +11,7 @@ import Foundation
|
||||||
struct MastodonError: Decodable, CustomStringConvertible {
|
struct MastodonError: Decodable, CustomStringConvertible {
|
||||||
var description: String
|
var description: String
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
if let error = try container.decodeIfPresent(String.self, forKey: .error) {
|
|
||||||
self.description = error
|
|
||||||
} else if let message = try container.decodeIfPresent(String.self, forKey: .message) {
|
|
||||||
self.description = message
|
|
||||||
} else {
|
|
||||||
throw DecodingError.keyNotFound(CodingKeys.error, .init(codingPath: container.codingPath, debugDescription: "Missing error or message key"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case error
|
case description = "error"
|
||||||
// used by pixelfed
|
|
||||||
case message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
//
|
|
||||||
// SearchOperatorType.swift
|
|
||||||
// Pachyderm
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/1/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public enum SearchOperatorType: String, CaseIterable, Equatable {
|
|
||||||
case has
|
|
||||||
case `is`
|
|
||||||
case language
|
|
||||||
case from
|
|
||||||
case before
|
|
||||||
case during
|
|
||||||
case after
|
|
||||||
case `in`
|
|
||||||
}
|
|
|
@ -10,9 +10,6 @@ import Foundation
|
||||||
import WebURL
|
import WebURL
|
||||||
|
|
||||||
public final class Status: StatusProtocol, Decodable, Sendable {
|
public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
/// The pseudo-visibility used by instance types (Akkoma) that overload the visibility for local-only posts.
|
|
||||||
public static let localPostVisibility: String = "local"
|
|
||||||
|
|
||||||
public let id: String
|
public let id: String
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let url: WebURL?
|
public let url: WebURL?
|
||||||
|
@ -80,7 +77,7 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly)
|
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly)
|
||||||
} else if let s = try? container.decode(String.self, forKey: .visibility),
|
} else if let s = try? container.decode(String.self, forKey: .visibility),
|
||||||
s == Status.localPostVisibility {
|
s == "local" {
|
||||||
// hacky workaround for #332, akkoma describes local posts with a separate visibility
|
// hacky workaround for #332, akkoma describes local posts with a separate visibility
|
||||||
self.visibility = .public
|
self.visibility = .public
|
||||||
self.localOnly = true
|
self.localOnly = true
|
||||||
|
|
|
@ -436,6 +436,7 @@ extension Preferences {
|
||||||
public enum FeatureFlag: String, Codable {
|
public enum FeatureFlag: String, Codable {
|
||||||
case iPadMultiColumn = "ipad-multi-column"
|
case iPadMultiColumn = "ipad-multi-column"
|
||||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
case iPadBrowserNavigation = "ipad-browser-navigation"
|
||||||
|
case iPadNavigationMode = "ipad-navigation-mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
#include "Version.xcconfig"
|
|
||||||
|
|
||||||
DEVELOPMENT_TEAM = YOUR_TEAM_ID
|
DEVELOPMENT_TEAM = YOUR_TEAM_ID
|
||||||
BUNDLE_ID_PREFIX = com.example
|
BUNDLE_ID_PREFIX = com.example
|
||||||
|
|
||||||
|
|
|
@ -286,9 +286,6 @@
|
||||||
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D6CA6ED129EF6091003EC5DF /* TuskerPreferences */; };
|
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D6CA6ED129EF6091003EC5DF /* TuskerPreferences */; };
|
||||||
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
||||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
|
||||||
D6CF5B832AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CF5B822AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift */; };
|
|
||||||
D6CF5B852AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */; };
|
|
||||||
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CF5B882AC9BA6E00F15D83 /* MastodonSearchController.swift */; };
|
|
||||||
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */; };
|
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */; };
|
||||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */; };
|
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */; };
|
||||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B57292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift */; };
|
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B57292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift */; };
|
||||||
|
@ -340,7 +337,6 @@
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
||||||
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; };
|
|
||||||
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
||||||
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */; };
|
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */; };
|
||||||
|
@ -652,7 +648,6 @@
|
||||||
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>"; };
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameView.swift; sourceTree = "<group>"; };
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameView.swift; sourceTree = "<group>"; };
|
||||||
D6B5F3BC2ACA586C00309734 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
|
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -690,9 +685,6 @@
|
||||||
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerPreferences; path = Packages/TuskerPreferences; sourceTree = "<group>"; };
|
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerPreferences; path = Packages/TuskerPreferences; sourceTree = "<group>"; };
|
||||||
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
||||||
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
|
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
|
||||||
D6CF5B822AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSection+Readable.swift"; sourceTree = "<group>"; };
|
|
||||||
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiColumnCollectionViewLayout.swift; sourceTree = "<group>"; };
|
|
||||||
D6CF5B882AC9BA6E00F15D83 /* MastodonSearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonSearchController.swift; sourceTree = "<group>"; };
|
|
||||||
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineGapCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineGapCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D12B57292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListCollectionViewController.swift; sourceTree = "<group>"; };
|
D6D12B57292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -754,7 +746,6 @@
|
||||||
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; };
|
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; };
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTokenSuggestionCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = "<group>"; };
|
||||||
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBackgroundConfiguration+AppColors.swift"; sourceTree = "<group>"; };
|
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBackgroundConfiguration+AppColors.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1254,7 +1245,6 @@
|
||||||
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
|
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
|
||||||
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */,
|
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */,
|
||||||
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */,
|
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */,
|
||||||
D6CF5B822AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift */,
|
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1369,8 +1359,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
||||||
D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */,
|
|
||||||
D6CF5B882AC9BA6E00F15D83 /* MastodonSearchController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Search;
|
path = Search;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1461,7 +1449,6 @@
|
||||||
children = (
|
children = (
|
||||||
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
||||||
D6D706A829498C82000827ED /* Tusker.xcconfig */,
|
D6D706A829498C82000827ED /* Tusker.xcconfig */,
|
||||||
D6B5F3BC2ACA586C00309734 /* Version.xcconfig */,
|
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */,
|
D674A50727F910F300BA03AC /* Pachyderm */,
|
||||||
D6BEA243291A0C83002F4D01 /* Duckable */,
|
D6BEA243291A0C83002F4D01 /* Duckable */,
|
||||||
D68A76F22953915C001DA1B3 /* TTTKit */,
|
D68A76F22953915C001DA1B3 /* TTTKit */,
|
||||||
|
@ -1512,7 +1499,6 @@
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D61DC84528F498F200B82C6E /* Logging.swift */,
|
D61DC84528F498F200B82C6E /* Logging.swift */,
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||||
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
|
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
|
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
|
||||||
|
@ -1784,9 +1770,8 @@
|
||||||
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
|
||||||
LastSwiftUpdateCheck = 1430;
|
LastSwiftUpdateCheck = 1430;
|
||||||
LastUpgradeCheck = 1500;
|
LastUpgradeCheck = 1400;
|
||||||
ORGANIZATIONNAME = Shadowfacts;
|
ORGANIZATIONNAME = Shadowfacts;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
D6A4531229EF64BA00032932 = {
|
D6A4531229EF64BA00032932 = {
|
||||||
|
@ -2092,7 +2077,6 @@
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
|
||||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
||||||
D6CF5B852AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift in Sources */,
|
|
||||||
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
D68A76EE295369C7001DA1B3 /* AcknowledgementsView.swift in Sources */,
|
||||||
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */,
|
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */,
|
||||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
||||||
|
@ -2138,7 +2122,6 @@
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
D61F75882932DB6000C0B37F /* StatusSwipeActions.swift in Sources */,
|
D61F75882932DB6000C0B37F /* StatusSwipeActions.swift in Sources */,
|
||||||
D68A76EA295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift in Sources */,
|
D68A76EA295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift in Sources */,
|
||||||
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */,
|
|
||||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift in Sources */,
|
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListCollectionViewController.swift in Sources */,
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||||
|
@ -2221,7 +2204,6 @@
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||||
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */,
|
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||||
|
@ -2235,7 +2217,6 @@
|
||||||
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
||||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||||
D6CF5B832AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift in Sources */,
|
|
||||||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
||||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
||||||
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
|
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
|
||||||
|
@ -2417,7 +2398,7 @@
|
||||||
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 = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 103;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2425,7 +2406,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = 2023.7;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -2483,7 +2464,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 = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2508,7 +2489,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2536,7 +2517,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2564,7 +2545,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2718,7 +2699,7 @@
|
||||||
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 = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 103;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2726,7 +2707,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = 2023.7;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
|
@ -2749,7 +2730,7 @@
|
||||||
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 = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 103;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2757,7 +2738,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = 2023.7;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -2855,7 +2836,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 = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2880,7 +2861,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 = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = 100;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1500"
|
LastUpgradeVersion = "1430"
|
||||||
wasCreatedForAppExtension = "YES"
|
wasCreatedForAppExtension = "YES"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1500"
|
LastUpgradeVersion = "1400"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
//
|
|
||||||
// NSCollectionLayoutSection+Readable.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/28/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension NSCollectionLayoutSection {
|
|
||||||
|
|
||||||
// The .readableContent insets reference has a bunch of weird behavior,
|
|
||||||
// so just calculate the content inset ourselves.
|
|
||||||
func readableContentInset(in environment: NSCollectionLayoutEnvironment) {
|
|
||||||
guard let maximumReadableWidth = environment.maximumReadableWidth else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let inset = max(0, (environment.container.contentSize.width - maximumReadableWidth) / 2)
|
|
||||||
// make sure not to overwrite the vertical insets, which are non-zero for grouped styles
|
|
||||||
contentInsets.leading = inset
|
|
||||||
contentInsets.trailing = inset
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NSCollectionLayoutEnvironment {
|
|
||||||
var maximumReadableWidth: CGFloat? {
|
|
||||||
switch traitCollection.preferredContentSizeCategory {
|
|
||||||
case .extraSmall:
|
|
||||||
return 560
|
|
||||||
case .small:
|
|
||||||
return 600
|
|
||||||
case .medium:
|
|
||||||
return 632
|
|
||||||
case .large:
|
|
||||||
return 664
|
|
||||||
case .extraLarge:
|
|
||||||
return 744
|
|
||||||
case .extraExtraLarge:
|
|
||||||
return 816
|
|
||||||
case .extraExtraExtraLarge:
|
|
||||||
return 896
|
|
||||||
case .accessibilityMedium:
|
|
||||||
return 1096
|
|
||||||
default:
|
|
||||||
// greater accessibility sizes don't have a limit
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -37,12 +37,7 @@ struct HTMLConverter {
|
||||||
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
mutAttrString.collapseWhitespace()
|
mutAttrString.collapseWhitespace()
|
||||||
|
|
||||||
// Wait until the end and then fill in the unset paragraph styles, to avoid clobbering the list style.
|
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: mutAttrString.fullRange)
|
||||||
mutAttrString.enumerateAttribute(.paragraphStyle, in: mutAttrString.fullRange, options: .longestEffectiveRangeNotRequired) { value, range, stop in
|
|
||||||
if value == nil {
|
|
||||||
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mutAttrString
|
return mutAttrString
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,10 +56,6 @@ struct HTMLConverter {
|
||||||
}
|
}
|
||||||
return NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: color])
|
return NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: color])
|
||||||
case let node as Element:
|
case let node as Element:
|
||||||
if node.tagName() == "ol" || node.tagName() == "ul" {
|
|
||||||
return attributedTextForList(node, usePreformattedText: usePreformattedText)
|
|
||||||
}
|
|
||||||
|
|
||||||
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
||||||
for child in node.getChildNodes() {
|
for child in node.getChildNodes() {
|
||||||
var appendEllipsis = false
|
var appendEllipsis = false
|
||||||
|
@ -124,6 +115,25 @@ struct HTMLConverter {
|
||||||
case "pre":
|
case "pre":
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
|
attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
|
||||||
|
case "ol", "ul":
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
|
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
case "li":
|
||||||
|
let parentEl = node.parent()!
|
||||||
|
let parentTag = parentEl.tagName()
|
||||||
|
let bullet: NSAttributedString
|
||||||
|
if parentTag == "ol" {
|
||||||
|
let index = (try? node.elementSiblingIndex()) ?? 0
|
||||||
|
// we use the monospace digit font so that the periods of all the list items line up
|
||||||
|
// TODO: this probably breaks with dynamic type
|
||||||
|
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: monospaceFont, .foregroundColor: color])
|
||||||
|
} else if parentTag == "ul" {
|
||||||
|
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: font, .foregroundColor: color])
|
||||||
|
} else {
|
||||||
|
bullet = NSAttributedString()
|
||||||
|
}
|
||||||
|
attributed.insert(bullet, at: 0)
|
||||||
|
attributed.append(NSAttributedString(string: "\n", attributes: [.font: font]))
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -134,37 +144,5 @@ struct HTMLConverter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attributedTextForList(_ element: Element, usePreformattedText: Bool) -> NSAttributedString {
|
|
||||||
let list = element.tagName() == "ol" ? OrderedNumberTextList(markerFormat: .decimal, options: 0) : NSTextList(markerFormat: .disc, options: 0)
|
|
||||||
let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
|
|
||||||
// I don't like that I can't just use paragraphStyle.textLists, because it makes the list markers
|
|
||||||
// not use the monospace digit font (it seems to just use whatever font attribute is set for the whole thing),
|
|
||||||
// and it doesn't right align the list markers.
|
|
||||||
// Unfortunately, doing it manually means the list markers are incldued in the selectable text.
|
|
||||||
paragraphStyle.headIndent = 32
|
|
||||||
paragraphStyle.firstLineHeadIndent = 0
|
|
||||||
// Use 2 tab stops, one for the list marker, the second for the content.
|
|
||||||
paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)]
|
|
||||||
let str = NSMutableAttributedString(string: "")
|
|
||||||
var item = 1
|
|
||||||
for child in element.children() where child.tagName() == "li" {
|
|
||||||
if let childStr = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText) {
|
|
||||||
str.append(NSAttributedString(string: "\t\(list.marker(forItemNumber: item))\t", attributes: [
|
|
||||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .monospacedDigitSystemFont(ofSize: 17, weight: .regular)),
|
|
||||||
]))
|
|
||||||
str.append(childStr)
|
|
||||||
str.append(NSAttributedString(string: "\n"))
|
|
||||||
item += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
str.addAttribute(.paragraphStyle, value: paragraphStyle, range: str.fullRange)
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class OrderedNumberTextList: NSTextList {
|
|
||||||
override func marker(forItemNumber itemNumber: Int) -> String {
|
|
||||||
"\(super.marker(forItemNumber: itemNumber))."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,260 +0,0 @@
|
||||||
//
|
|
||||||
// MultiColumnCollectionViewLayout.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/29/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class MultiColumnCollectionViewLayout: UICollectionViewLayout {
|
|
||||||
|
|
||||||
private let numberOfColumns: Int
|
|
||||||
private let spacing: CGFloat
|
|
||||||
private let minimumColumnWidth: CGFloat
|
|
||||||
private var effectiveNumberOfColumns: Int!
|
|
||||||
|
|
||||||
private var attributes: [MultiColumnLayoutAttributes] = []
|
|
||||||
private var invalidatedItemIndices: IndexSet = []
|
|
||||||
|
|
||||||
var showSectionHeader = false {
|
|
||||||
didSet {
|
|
||||||
if showSectionHeader != oldValue {
|
|
||||||
invalidateLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var sectionHeaderAttributes: MultiColumnLayoutAttributes?
|
|
||||||
|
|
||||||
init(numberOfColumns: Int, columnSpacing: CGFloat, minimumColumnWidth: CGFloat) {
|
|
||||||
precondition(numberOfColumns >= 1)
|
|
||||||
self.numberOfColumns = numberOfColumns
|
|
||||||
self.effectiveNumberOfColumns = nil
|
|
||||||
self.spacing = columnSpacing
|
|
||||||
self.minimumColumnWidth = minimumColumnWidth
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override class var layoutAttributesClass: AnyClass {
|
|
||||||
MultiColumnLayoutAttributes.self
|
|
||||||
}
|
|
||||||
|
|
||||||
override class var invalidationContextClass: AnyClass {
|
|
||||||
MultiColumnLayoutInvalidationContext.self
|
|
||||||
}
|
|
||||||
|
|
||||||
override func prepare() {
|
|
||||||
guard let collectionView else { return }
|
|
||||||
precondition(collectionView.numberOfSections <= 1)
|
|
||||||
|
|
||||||
guard collectionView.numberOfSections == 1 else {
|
|
||||||
attributes = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if effectiveNumberOfColumns == nil {
|
|
||||||
updateEffectiveNumberOfColumns()
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastAttributesInEachColumn: [MultiColumnLayoutAttributes?] = Array(repeating: nil, count: effectiveNumberOfColumns)
|
|
||||||
func columnWithMinHeight() -> Int {
|
|
||||||
var min: Int?
|
|
||||||
for i in 0..<effectiveNumberOfColumns {
|
|
||||||
guard let attrs = lastAttributesInEachColumn[i] else {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
if min == nil || attrs.frame.maxY < lastAttributesInEachColumn[min!]!.frame.maxY {
|
|
||||||
min = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return min!
|
|
||||||
}
|
|
||||||
|
|
||||||
let columnWidth = columnWidthForColumnCount(effectiveNumberOfColumns)
|
|
||||||
func minXForColumn(_ column: Int) -> CGFloat {
|
|
||||||
(CGFloat(column) * columnWidth) + ((CGFloat(column) + 1) * spacing)
|
|
||||||
}
|
|
||||||
|
|
||||||
let startY: CGFloat
|
|
||||||
if showSectionHeader {
|
|
||||||
let indexPath = IndexPath(item: 0, section: 0)
|
|
||||||
if sectionHeaderAttributes == nil {
|
|
||||||
sectionHeaderAttributes = .init(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: indexPath)
|
|
||||||
}
|
|
||||||
sectionHeaderAttributes!.frame = CGRect(x: 0, y: 0, width: collectionView.bounds.width, height: 0)
|
|
||||||
if let view = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: indexPath) {
|
|
||||||
let preferred = view.preferredLayoutAttributesFitting(sectionHeaderAttributes!)
|
|
||||||
sectionHeaderAttributes!.frame.size.height = preferred.frame.height
|
|
||||||
}
|
|
||||||
startY = sectionHeaderAttributes!.frame.height
|
|
||||||
} else {
|
|
||||||
startY = spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
let numberOfItems = collectionView.numberOfItems(inSection: 0)
|
|
||||||
for item in 0..<numberOfItems {
|
|
||||||
let column = columnWithMinHeight()
|
|
||||||
|
|
||||||
let itemAttrs: MultiColumnLayoutAttributes
|
|
||||||
if attributes.count > item {
|
|
||||||
itemAttrs = attributes[item]
|
|
||||||
} else {
|
|
||||||
itemAttrs = MultiColumnLayoutAttributes(forCellWith: IndexPath(item: item, section: 0))
|
|
||||||
// estimate
|
|
||||||
itemAttrs.frame.size.height = 200
|
|
||||||
attributes.append(itemAttrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemAttrs.column = column
|
|
||||||
itemAttrs.frame.size.width = columnWidth
|
|
||||||
|
|
||||||
if invalidatedItemIndices.contains(item) {
|
|
||||||
if let cell = collectionView.cellForItem(at: IndexPath(item: item, section: 0)) {
|
|
||||||
let preferred = cell.preferredLayoutAttributesFitting(itemAttrs.copy() as! UICollectionViewLayoutAttributes)
|
|
||||||
itemAttrs.frame.size.height = preferred.frame.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let lastInColumn = lastAttributesInEachColumn[column] {
|
|
||||||
itemAttrs.frame.origin = CGPoint(
|
|
||||||
x: lastInColumn.frame.minX,
|
|
||||||
y: lastInColumn.frame.maxY + spacing
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
itemAttrs.frame.origin = CGPoint(
|
|
||||||
x: minXForColumn(column),
|
|
||||||
y: startY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastAttributesInEachColumn[column] = itemAttrs
|
|
||||||
}
|
|
||||||
|
|
||||||
if attributes.count > numberOfItems {
|
|
||||||
attributes.removeLast(attributes.count - numberOfItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidatedItemIndices = []
|
|
||||||
}
|
|
||||||
|
|
||||||
override var collectionViewContentSize: CGSize {
|
|
||||||
guard let collectionView else {
|
|
||||||
return .zero
|
|
||||||
}
|
|
||||||
let maxY = attributes.lazy.map(\.frame.maxY).max()
|
|
||||||
return CGSize(width: collectionView.bounds.width, height: (maxY ?? 0) + spacing)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
||||||
return attributes[indexPath.item]
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
|
||||||
// TODO: optimize this
|
|
||||||
var attributes = attributes.filter { $0.frame.intersects(rect) }
|
|
||||||
if showSectionHeader,
|
|
||||||
let sectionHeaderAttributes,
|
|
||||||
rect.minY <= sectionHeaderAttributes.frame.maxY {
|
|
||||||
attributes.append(sectionHeaderAttributes)
|
|
||||||
}
|
|
||||||
return attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
||||||
if elementKind == UICollectionView.elementKindSectionHeader,
|
|
||||||
showSectionHeader {
|
|
||||||
return sectionHeaderAttributes
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func indexPathsToInsertForSupplementaryView(ofKind elementKind: String) -> [IndexPath] {
|
|
||||||
if elementKind == UICollectionView.elementKindSectionHeader {
|
|
||||||
return [IndexPath(item: 0, section: 0)]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
||||||
return newBounds.width != collectionView?.bounds.width
|
|
||||||
}
|
|
||||||
|
|
||||||
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
|
|
||||||
let context = super.invalidationContext(forBoundsChange: newBounds) as! MultiColumnLayoutInvalidationContext
|
|
||||||
context.updateEffectiveNumberOfColumns = true
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// On landscape-to-portrait device rotations (though seemingly only when inside a UISplitViewController), the collection view
|
|
||||||
// doesn't go through the bounds-change codepath. But we may still need to update the effective number of columns,
|
|
||||||
// so override this private method (which is called) and configure the invalidation context appropriately.
|
|
||||||
override func _invalidationContext(forUpdatedLayoutMargins margins: UIEdgeInsets) -> UICollectionViewLayoutInvalidationContext {
|
|
||||||
let context = super._invalidationContext(forUpdatedLayoutMargins: margins) as! MultiColumnLayoutInvalidationContext
|
|
||||||
context.updateEffectiveNumberOfColumns = true
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool {
|
|
||||||
return preferredAttributes.frame.height != originalAttributes.frame.height
|
|
||||||
}
|
|
||||||
|
|
||||||
override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {
|
|
||||||
let context = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
|
|
||||||
if let kind = preferredAttributes.representedElementKind {
|
|
||||||
context.invalidateSupplementaryElements(ofKind: kind, at: [preferredAttributes.indexPath])
|
|
||||||
} else {
|
|
||||||
context.invalidateItems(at: [preferredAttributes.indexPath])
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
|
|
||||||
let context = context as! MultiColumnLayoutInvalidationContext
|
|
||||||
|
|
||||||
for indexPath in context.invalidatedItemIndexPaths ?? [] {
|
|
||||||
invalidatedItemIndices.insert(indexPath.item)
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.invalidateEverything || context.updateEffectiveNumberOfColumns {
|
|
||||||
updateEffectiveNumberOfColumns()
|
|
||||||
}
|
|
||||||
|
|
||||||
super.invalidateLayout(with: context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func columnWidthForColumnCount(_ count: Int) -> CGFloat {
|
|
||||||
guard let collectionView else { return 0 }
|
|
||||||
let spacingTotal = spacing * (CGFloat(count) + 1)
|
|
||||||
return (collectionView.bounds.width - spacingTotal) / CGFloat(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateEffectiveNumberOfColumns() {
|
|
||||||
var n = numberOfColumns
|
|
||||||
while n > 1 && columnWidthForColumnCount(n) < minimumColumnWidth {
|
|
||||||
n -= 1
|
|
||||||
}
|
|
||||||
effectiveNumberOfColumns = n
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MultiColumnLayoutAttributes: UICollectionViewLayoutAttributes {
|
|
||||||
var column: Int = -1
|
|
||||||
|
|
||||||
override func copy(with zone: NSZone? = nil) -> Any {
|
|
||||||
let copy = super.copy(with: zone) as! MultiColumnLayoutAttributes
|
|
||||||
copy.column = column
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MultiColumnLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext {
|
|
||||||
var updateEffectiveNumberOfColumns: Bool = false
|
|
||||||
}
|
|
|
@ -91,10 +91,7 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTitle(draft: Draft) {
|
private func updateTitle(draft: Draft) {
|
||||||
// Don't set the scene title on macOS since it shows both that and the VC title in the window titlebar
|
guard let scene = window?.windowScene,
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
guard !ProcessInfo.processInfo.isiOSAppOnMac,
|
|
||||||
let scene = window?.windowScene,
|
|
||||||
let mastodonController = scene.session.mastodonController else {
|
let mastodonController = scene.session.mastodonController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -104,7 +101,6 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
|
||||||
} else {
|
} else {
|
||||||
scene.title = "New Post"
|
scene.title = "New Post"
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func themePrefChanged() {
|
@objc private func themePrefChanged() {
|
||||||
|
|
|
@ -56,7 +56,9 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -35,7 +35,9 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
config.backgroundColor = .appGroupedBackground
|
config.backgroundColor = .appGroupedBackground
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -59,12 +59,10 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
|
||||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
// we're not using contenetInsetsReference = .readableContent here because it always insets the cells even if
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
// the collection view's actual width is narrow enough to fit in the readable width, resulting in a bit of the
|
||||||
section.readableContentInset(in: environment)
|
// background color always peeking through the edges
|
||||||
return section
|
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||||
}
|
|
||||||
viewRespectsSystemMinimumLayoutMargins = false
|
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
// something about the autoresizing mask breaks resizing the vc
|
// something about the autoresizing mask breaks resizing the vc
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -59,7 +59,14 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController!
|
resultsController.exploreNavigationController = self.navigationController!
|
||||||
searchController = MastodonSearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
|
searchController.searchResultsUpdater = resultsController
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
searchController.scopeBarActivation = .onSearchActivation
|
||||||
|
}
|
||||||
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
|
searchController.searchBar.delegate = resultsController
|
||||||
|
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
@ -81,6 +88,13 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
)
|
)
|
||||||
.sink { [unowned self] in self.updateHashtagsSection(followed: $0) }
|
.sink { [unowned self] in self.updateHashtagsSection(followed: $0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
let a = PassthroughSubject<Int, Never>()
|
||||||
|
let b = PassthroughSubject<Int, Never>()
|
||||||
|
|
||||||
|
a.merge(with: b)
|
||||||
|
.sink(receiveValue: { print($0) })
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
|
|
@ -34,8 +34,14 @@ class InlineTrendsViewController: UIViewController {
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController
|
resultsController.exploreNavigationController = self.navigationController
|
||||||
searchController = MastodonSearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
searchController.obscuresBackgroundDuringPresentation = true
|
searchController.obscuresBackgroundDuringPresentation = true
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
searchController.scopeBarActivation = .onSearchActivation
|
||||||
|
}
|
||||||
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
|
searchController.searchBar.delegate = resultsController
|
||||||
|
searchController.searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
|
|
|
@ -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="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="collection view cell content view" 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"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalHuggingPriority="249" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FJh-fd-fo8" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FJh-fd-fo8" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="8" y="147" width="431" height="170"/>
|
<rect key="frame" x="8" y="147" width="431" height="170"/>
|
||||||
<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"/>
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<image name="clock.arrow.circlepath" catalog="system" width="128" height="112"/>
|
<image name="clock.arrow.circlepath" catalog="system" width="128" height="112"/>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</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"/>
|
||||||
|
|
|
@ -14,7 +14,6 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var collectionView: UICollectionView!
|
var collectionView: UICollectionView!
|
||||||
private var layout: MultiColumnCollectionViewLayout!
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var state = State.unloaded
|
private var state = State.unloaded
|
||||||
|
@ -34,7 +33,30 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
|
|
||||||
title = "Suggested Accounts"
|
title = "Suggested Accounts"
|
||||||
|
|
||||||
layout = MultiColumnCollectionViewLayout(numberOfColumns: 2, columnSpacing: 16, minimumColumnWidth: 320)
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
|
switch dataSource.sectionIdentifier(for: sectionIndex) {
|
||||||
|
case nil:
|
||||||
|
fatalError()
|
||||||
|
|
||||||
|
case .loadingIndicator:
|
||||||
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
config.backgroundColor = .appGroupedBackground
|
||||||
|
config.showsSeparators = false
|
||||||
|
return .list(using: config, layoutEnvironment: environment)
|
||||||
|
|
||||||
|
case .accounts:
|
||||||
|
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: size)
|
||||||
|
let item2 = NSCollectionLayoutItem(layoutSize: size)
|
||||||
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item, item2])
|
||||||
|
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
|
||||||
|
group.interItemSpacing = .fixed(16)
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.interGroupSpacing = 16
|
||||||
|
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
}
|
||||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
@ -53,27 +75,21 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
|
cell.indicator.startAnimating()
|
||||||
|
}
|
||||||
let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
|
let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: item.0, source: item.1)
|
cell.updateUI(accountID: item.0, source: item.1)
|
||||||
}
|
}
|
||||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
case .account(let id, let source):
|
case .account(let id, let source):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
|
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: (id, source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let loadingView = UICollectionView.SupplementaryRegistration<LoadingCollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader) { view, elementKind, indexPath in
|
|
||||||
view.indicator.startAnimating()
|
|
||||||
}
|
|
||||||
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
|
|
||||||
if elementKind == UICollectionView.elementKindSectionHeader {
|
|
||||||
return collectionView.dequeueConfiguredReusableSupplementary(using: loadingView, for: indexPath)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataSource
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -91,10 +107,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
}
|
}
|
||||||
state = .loading
|
state = .loading
|
||||||
|
|
||||||
layout.showSectionHeader = true
|
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.accounts])
|
snapshot.appendSections([.loadingIndicator])
|
||||||
|
snapshot.appendItems([.loadingIndicator])
|
||||||
await dataSource.apply(snapshot)
|
await dataSource.apply(snapshot)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -103,8 +118,6 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
|
|
||||||
await mastodonController.persistentContainer.addAll(accounts: suggestions.map(\.account))
|
await mastodonController.persistentContainer.addAll(accounts: suggestions.map(\.account))
|
||||||
|
|
||||||
layout.showSectionHeader = false
|
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.accounts])
|
snapshot.appendSections([.accounts])
|
||||||
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
|
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
|
||||||
|
@ -118,7 +131,6 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
|
||||||
await self?.loadInitial()
|
await self?.loadInitial()
|
||||||
}
|
}
|
||||||
showToast(configuration: config, animated: true)
|
showToast(configuration: config, animated: true)
|
||||||
layout.showSectionHeader = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,9 +146,11 @@ extension SuggestedProfilesViewController {
|
||||||
|
|
||||||
extension SuggestedProfilesViewController {
|
extension SuggestedProfilesViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
|
case loadingIndicator
|
||||||
case accounts
|
case accounts
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
|
case loadingIndicator
|
||||||
case account(String, Suggestion.Source)
|
case account(String, Suggestion.Source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,17 +50,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
case .links:
|
case .links:
|
||||||
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
|
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(280))
|
||||||
let group: NSCollectionLayoutGroup
|
let item = NSCollectionLayoutItem(layoutSize: size)
|
||||||
if let maximumReadableWidth = environment.maximumReadableWidth,
|
let item2 = NSCollectionLayoutItem(layoutSize: size)
|
||||||
environment.container.contentSize.width >= maximumReadableWidth {
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: size, subitems: [item, item2])
|
||||||
let width = (environment.container.contentSize.width - 48) / 2
|
|
||||||
group = .horizontal(layoutSize: size, subitems: [
|
|
||||||
NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(width), heightDimension: .estimated(280))),
|
|
||||||
NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(width), heightDimension: .estimated(280))),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
group = .vertical(layoutSize: size, subitems: [NSCollectionLayoutItem(layoutSize: size)])
|
|
||||||
}
|
|
||||||
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
|
group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
|
||||||
group.interItemSpacing = .fixed(16)
|
group.interItemSpacing = .fixed(16)
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
|
@ -62,7 +62,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -16,7 +16,7 @@ protocol LargeImageContentView: UIView {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var activityItemsForSharing: [Any] { get }
|
var activityItemsForSharing: [Any] { get }
|
||||||
var owner: LargeImageViewController? { get set }
|
var owner: LargeImageViewController? { get set }
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool)
|
func setControlsVisible(_ controlsVisible: Bool)
|
||||||
func grayscaleStateChanged()
|
func grayscaleStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,11 +75,13 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
func setControlsVisible(_ controlsVisible: Bool) {
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
let analysisInteraction {
|
let analysisInteraction {
|
||||||
analysisInteraction.setSupplementaryInterfaceHidden(!controlsVisible, animated: animated)
|
// note: passing animated: true here doesn't seem to do anything by itself as of iOS 16.2 (20C5032e)
|
||||||
|
// so the LargeImageViewController handles animating, but we still need to pass true here otherwise it doesn't animate
|
||||||
|
analysisInteraction.setSupplementaryInterfaceHidden(!controlsVisible, animated: true)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -136,7 +138,7 @@ class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
func setControlsVisible(_ controlsVisible: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
|
@ -187,7 +189,7 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
func setControlsVisible(_ controlsVisible: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
|
|
|
@ -282,17 +282,13 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
self.controlsVisible = controlsVisible
|
self.controlsVisible = controlsVisible
|
||||||
if animated {
|
if animated {
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
// note: the value of animated: is passed to ImageAnalysisInteractino.setSupplementaryInterfaceHidden which (as of iOS 17.0.2 (21A350)):
|
self.contentView.setControlsVisible(controlsVisible)
|
||||||
// - does not animate with animated:false when wrapped in a UIView.animate block
|
|
||||||
// - does not animate with animated:true unless also wrapped in a UIView.animate block
|
|
||||||
self.contentView.setControlsVisible(controlsVisible, animated: true)
|
|
||||||
self.updateControlsView()
|
self.updateControlsView()
|
||||||
}
|
}
|
||||||
if controlsVisible && !descriptionTextView.isHidden {
|
if controlsVisible && !descriptionTextView.isHidden {
|
||||||
descriptionTextView.flashScrollIndicators()
|
descriptionTextView.flashScrollIndicators()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentView.setControlsVisible(controlsVisible, animated: false)
|
|
||||||
updateControlsView()
|
updateControlsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,9 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -36,7 +36,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(item: MainSidebarViewController.Item, account: UserAccountInfo) {
|
func updateUI(item: MainSidebarViewController.Item, account: UserAccountInfo) async {
|
||||||
var config = defaultContentConfiguration()
|
var config = defaultContentConfiguration()
|
||||||
config.text = item.title
|
config.text = item.title
|
||||||
config.image = UIImage(systemName: item.imageName!)
|
config.image = UIImage(systemName: item.imageName!)
|
||||||
|
@ -50,12 +50,6 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
|
||||||
await updateAvatar(account: account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateAvatar(account: UserAccountInfo) async {
|
|
||||||
let mastodonController = MastodonController.getForAccount(account)
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
guard let account = try? await mastodonController.getOwnAccount(),
|
guard let account = try? await mastodonController.getOwnAccount(),
|
||||||
let avatar = account.avatar else {
|
let avatar = account.avatar else {
|
||||||
|
|
|
@ -139,7 +139,9 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
let myProfileCell = UICollectionView.CellRegistration<MainSidebarMyProfileCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
let myProfileCell = UICollectionView.CellRegistration<MainSidebarMyProfileCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
||||||
cell.updateUI(item: item, account: self.mastodonController.accountInfo!)
|
Task {
|
||||||
|
await cell.updateUI(item: item, account: self.mastodonController.accountInfo!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||||
|
|
|
@ -117,8 +117,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
guard mode != navigationMode else {
|
guard mode != navigationMode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigationMode = mode
|
|
||||||
|
|
||||||
let viewControllers = secondaryNavController.viewControllers
|
let viewControllers = secondaryNavController.viewControllers
|
||||||
secondaryNavController.viewControllers = []
|
secondaryNavController.viewControllers = []
|
||||||
// Setting viewControllers = [] doesn't remove the VC views from their superviews immediately,
|
// Setting viewControllers = [] doesn't remove the VC views from their superviews immediately,
|
||||||
|
|
|
@ -139,25 +139,19 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
mastodonController.persistentContainer.account(for: $0.account.id)
|
mastodonController.persistentContainer.account(for: $0.account.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleAvatars = Array(people.lazy.compactMap(\.avatar).prefix(10))
|
avatarStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
for (index, avatarURL) in visibleAvatars.enumerated() {
|
for avatarURL in people.lazy.compactMap(\.avatar).prefix(10) {
|
||||||
let imageView: CachedImageView
|
let imageView = CachedImageView(cache: .avatars)
|
||||||
if index < avatarStack.arrangedSubviews.count {
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView = avatarStack.arrangedSubviews[index] as! CachedImageView
|
imageView.layer.masksToBounds = true
|
||||||
} else {
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
imageView = CachedImageView(cache: .avatars)
|
imageView.layer.cornerCurve = .continuous
|
||||||
imageView.contentMode = .scaleAspectFill
|
avatarStack.addArrangedSubview(imageView)
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
|
||||||
imageView.layer.cornerCurve = .continuous
|
|
||||||
avatarStack.addArrangedSubview(imageView)
|
|
||||||
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
|
|
||||||
}
|
|
||||||
imageView.update(for: avatarURL)
|
imageView.update(for: avatarURL)
|
||||||
}
|
}
|
||||||
while avatarStack.arrangedSubviews.count > visibleAvatars.count {
|
NSLayoutConstraint.activate(avatarStack.arrangedSubviews.map {
|
||||||
avatarStack.arrangedSubviews.last!.removeFromSuperview()
|
$0.widthAnchor.constraint(equalTo: $0.heightAnchor)
|
||||||
}
|
})
|
||||||
|
|
||||||
actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id)
|
actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id)
|
||||||
|
|
||||||
|
|
|
@ -116,25 +116,19 @@ class FollowNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
}, identifier: group.id)
|
}, identifier: group.id)
|
||||||
updateTimestamp()
|
updateTimestamp()
|
||||||
|
|
||||||
let visibleAvatars = Array(people.lazy.compactMap(\.avatar).prefix(10))
|
avatarStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
for (index, avatarURL) in visibleAvatars.enumerated() {
|
for avatarURL in people.lazy.compactMap(\.avatar).prefix(10) {
|
||||||
let imageView: CachedImageView
|
let imageView = CachedImageView(cache: .avatars)
|
||||||
if index < avatarStack.arrangedSubviews.count {
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView = avatarStack.arrangedSubviews[index] as! CachedImageView
|
imageView.layer.masksToBounds = true
|
||||||
} else {
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
imageView = CachedImageView(cache: .avatars)
|
imageView.layer.cornerCurve = .continuous
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
|
||||||
imageView.layer.cornerCurve = .continuous
|
|
||||||
avatarStack.addArrangedSubview(imageView)
|
|
||||||
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
|
|
||||||
}
|
|
||||||
imageView.update(for: avatarURL)
|
imageView.update(for: avatarURL)
|
||||||
|
avatarStack.addArrangedSubview(imageView)
|
||||||
}
|
}
|
||||||
while avatarStack.arrangedSubviews.count > visibleAvatars.count {
|
NSLayoutConstraint.activate(avatarStack.arrangedSubviews.map {
|
||||||
avatarStack.arrangedSubviews.last!.removeFromSuperview()
|
$0.widthAnchor.constraint(equalTo: $0.heightAnchor)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString {
|
private func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString {
|
||||||
|
|
|
@ -25,8 +25,6 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
private(set) var collectionView: UICollectionView!
|
private(set) var collectionView: UICollectionView!
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
|
|
||||||
|
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
@ -93,7 +91,9 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
@ -662,13 +662,6 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
||||||
if reconfigureVisibleItemsOnEndDecelerating {
|
|
||||||
reconfigureVisibleItemsOnEndDecelerating = false
|
|
||||||
reconfigureVisibleCells()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsCollectionViewController: UICollectionViewDragDelegate {
|
extension NotificationsCollectionViewController: UICollectionViewDragDelegate {
|
||||||
|
|
|
@ -90,11 +90,11 @@ struct AppearancePrefsView : View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var interfaceSection: some View {
|
private var interfaceSection: some View {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if preferences.hasFeatureFlag(.iPadNavigationMode),
|
||||||
|
UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
Section(header: Text("Interface")) {
|
Section(header: Text("Interface")) {
|
||||||
WidescreenNavigationPrefsView()
|
WidescreenNavigationPrefsView()
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
||||||
|
|
||||||
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
|
|
||||||
|
|
||||||
private(set) var state: State = .unloaded
|
private(set) var state: State = .unloaded
|
||||||
|
|
||||||
init(accountID: String?, kind: Kind, owner: ProfileViewController) {
|
init(accountID: String?, kind: Kind, owner: ProfileViewController) {
|
||||||
|
@ -91,7 +89,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
return .list(using: config, layoutEnvironment: environment)
|
return .list(using: config, layoutEnvironment: environment)
|
||||||
} else {
|
} else {
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -627,13 +627,6 @@ extension ProfileStatusesViewController: UICollectionViewDelegate {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
||||||
if reconfigureVisibleItemsOnEndDecelerating {
|
|
||||||
reconfigureVisibleItemsOnEndDecelerating = false
|
|
||||||
reconfigureVisibleCells()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UICollectionViewDragDelegate {
|
extension ProfileStatusesViewController: UICollectionViewDragDelegate {
|
||||||
|
|
|
@ -1,174 +0,0 @@
|
||||||
//
|
|
||||||
// MastodonSearchController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/1/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
private let acctRegex = try! NSRegularExpression(pattern: "[a-z0-9_]+(@[a-z0-9\\-\\.]+[a-z0-9]+)?$", options: .caseInsensitive)
|
|
||||||
private let dateLikeRegex = try! NSRegularExpression(pattern: "\\d{4}(-\\d{2}(-\\d{2})?)?$")
|
|
||||||
private let languageRegex = try! NSRegularExpression(pattern: "(?:language:)?(\\w{2,3})$", options: .caseInsensitive)
|
|
||||||
|
|
||||||
class MastodonSearchController: UISearchController {
|
|
||||||
override var delegate: UISearchControllerDelegate? {
|
|
||||||
willSet {
|
|
||||||
precondition(newValue === self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var searchResultsController: SearchResultsViewController {
|
|
||||||
super.searchResultsController as! SearchResultsViewController
|
|
||||||
}
|
|
||||||
|
|
||||||
init(searchResultsController: SearchResultsViewController) {
|
|
||||||
super.init(searchResultsController: searchResultsController)
|
|
||||||
|
|
||||||
searchResultsController.tokenHandler = { [unowned self] token, op in
|
|
||||||
self.addToken(token, operator: op)
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate = self
|
|
||||||
searchResultsUpdater = searchResultsController
|
|
||||||
automaticallyShowsSearchResultsController = false
|
|
||||||
showsSearchResultsController = true
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
scopeBarActivation = .onSearchActivation
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBar.autocapitalizationType = .none
|
|
||||||
searchBar.delegate = self
|
|
||||||
searchBar.scopeButtonTitles = SearchResultsViewController.Scope.allCases.map(\.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateTokenSuggestions(searchText: String, animated: Bool) {
|
|
||||||
guard searchResultsController.mastodonController.instanceFeatures.searchOperators else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchText = searchText.trimmingCharacters(in: .whitespaces)
|
|
||||||
|
|
||||||
var suggestions: [(SearchOperatorType, [String])] = []
|
|
||||||
|
|
||||||
suggestions.append((.has, ["has:media", "has:poll", "has:embed"].filter {
|
|
||||||
searchText.isEmpty || $0.contains(searchText)
|
|
||||||
}))
|
|
||||||
|
|
||||||
suggestions.append((.is, ["is:reply", "is:sensitive"].filter {
|
|
||||||
searchText.isEmpty || $0.contains(searchText)
|
|
||||||
}))
|
|
||||||
|
|
||||||
// TODO: use default language from preferences
|
|
||||||
var langSuggestions = [String]()
|
|
||||||
if searchText.isEmpty || "language:en".contains(searchText) {
|
|
||||||
langSuggestions.append("language:en")
|
|
||||||
}
|
|
||||||
if searchText != "en",
|
|
||||||
let match = languageRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.utf16.count)) {
|
|
||||||
let identifier = (searchText as NSString).substring(with: match.range(at: 1))
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
if Locale.LanguageCode.isoLanguageCodes.contains(where: { $0.identifier == identifier }) {
|
|
||||||
langSuggestions.append("language:\(identifier)")
|
|
||||||
}
|
|
||||||
} else if searchText != "en" {
|
|
||||||
langSuggestions.append("language:\(searchText)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suggestions.append((.language, langSuggestions))
|
|
||||||
|
|
||||||
var fromSuggestions = [String]()
|
|
||||||
if searchText.isEmpty || "from:me".contains(searchText) {
|
|
||||||
fromSuggestions.append("from:me")
|
|
||||||
}
|
|
||||||
if searchText != "me",
|
|
||||||
let match = acctRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.utf16.count)) {
|
|
||||||
let matched = (searchText as NSString).substring(with: match.range)
|
|
||||||
fromSuggestions.append("from:\(matched)")
|
|
||||||
}
|
|
||||||
suggestions.append((.from, fromSuggestions))
|
|
||||||
|
|
||||||
let components = Calendar.current.dateComponents([.year, .month], from: Date())
|
|
||||||
for op in [SearchOperatorType.before, .during, .after] {
|
|
||||||
if searchText.isEmpty {
|
|
||||||
suggestions.append((op, ["\(op.rawValue):\(components.year!)-\(components.month!)"]))
|
|
||||||
} else if let match = dateLikeRegex.firstMatch(in: searchText, range: NSRange(location: 0, length: searchText.utf16.count)) {
|
|
||||||
let matched = (searchText as NSString).substring(with: match.range)
|
|
||||||
suggestions.append((op, ["\(op.rawValue):\(matched)"]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestions.append((.in, ["in:all", "in:library"].filter {
|
|
||||||
searchText.isEmpty || $0.contains(searchText)
|
|
||||||
}))
|
|
||||||
|
|
||||||
searchResultsController.updateTokenSuggestions(suggestions, animated: animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addToken(_ token: String, operator: SearchOperatorType) {
|
|
||||||
let field = searchBar.searchTextField
|
|
||||||
if field.tokens.contains(where: { ($0.representedObject as? String) == token }) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let searchToken = UISearchToken(icon: nil, text: token)
|
|
||||||
searchToken.representedObject = token
|
|
||||||
field.insertToken(searchToken, at: field.tokens.count)
|
|
||||||
field.text = ""
|
|
||||||
let tokenPos = field.positionOfToken(at: field.tokens.count - 1)
|
|
||||||
field.selectedTextRange = field.textRange(from: tokenPos, to: field.endOfDocument)
|
|
||||||
|
|
||||||
if let requiredScope = `operator`.requiredScope,
|
|
||||||
let index = searchBar.scopeButtonTitles?.firstIndex(of: requiredScope.title) {
|
|
||||||
searchBar.selectedScopeButtonIndex = index
|
|
||||||
searchBar(searchBar, selectedScopeButtonIndexDidChange: index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MastodonSearchController: UISearchControllerDelegate {
|
|
||||||
func willPresentSearchController(_ searchController: UISearchController) {
|
|
||||||
updateTokenSuggestions(searchText: "", animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MastodonSearchController: UISearchBarDelegate {
|
|
||||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
|
||||||
updateTokenSuggestions(searchText: searchText, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
|
||||||
searchResultsController.searchBarTextDidEndEditing(searchBar)
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
|
||||||
searchResultsController.searchBar(searchBar, selectedScopeButtonIndexDidChange: selectedScope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UISearchBar {
|
|
||||||
var searchQueryWithOperators: String {
|
|
||||||
var parts = searchTextField.tokens.compactMap { $0.representedObject as? String }
|
|
||||||
let query = text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
||||||
if !query.isEmpty {
|
|
||||||
parts.append(query)
|
|
||||||
}
|
|
||||||
return parts.joined(separator: " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension SearchOperatorType {
|
|
||||||
var requiredScope: SearchResultsViewController.Scope? {
|
|
||||||
switch self {
|
|
||||||
case .is, .from, .in:
|
|
||||||
return .posts
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,7 +33,6 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
weak var exploreNavigationController: UINavigationController?
|
weak var exploreNavigationController: UINavigationController?
|
||||||
weak var delegate: SearchResultsViewControllerDelegate?
|
weak var delegate: SearchResultsViewControllerDelegate?
|
||||||
var tokenHandler: ((String, SearchOperatorType) -> Void)?
|
|
||||||
|
|
||||||
var collectionView: UICollectionView! { view as? UICollectionView }
|
var collectionView: UICollectionView! { view as? UICollectionView }
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
@ -43,9 +42,8 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
/// Whether to limit results to accounts the users is following.
|
/// Whether to limit results to accounts the users is following.
|
||||||
var following: Bool? = nil
|
var following: Bool? = nil
|
||||||
|
|
||||||
private let searchSubject = PassthroughSubject<String?, Never>()
|
let searchSubject = PassthroughSubject<String?, Never>()
|
||||||
private var searchCancellable: AnyCancellable?
|
var currentQuery: String?
|
||||||
private var currentQuery: String?
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController, scope: Scope = .all) {
|
init(mastodonController: MastodonController, scope: Scope = .all) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -62,43 +60,29 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)!
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
switch sectionIdentifier {
|
config.backgroundColor = .appGroupedBackground
|
||||||
case .tokenSuggestions(_):
|
config.headerMode = .supplementary
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .absolute(30))
|
switch self.dataSource.sectionIdentifier(for: sectionIndex) {
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
|
||||||
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
|
||||||
section.interGroupSpacing = 8
|
|
||||||
section.contentInsets = NSDirectionalEdgeInsets(top: sectionIndex == 0 ? 16 : 4, leading: 16, bottom: 4, trailing: 16)
|
|
||||||
return section
|
|
||||||
|
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
||||||
config.backgroundColor = .appGroupedBackground
|
|
||||||
config.showsSeparators = false
|
config.showsSeparators = false
|
||||||
config.headerMode = .none
|
config.headerMode = .none
|
||||||
return .list(using: config, layoutEnvironment: environment)
|
case .statuses:
|
||||||
|
|
||||||
case .accounts, .hashtags, .statuses:
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
||||||
config.backgroundColor = .appGroupedBackground
|
|
||||||
config.headerMode = .supplementary
|
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||||
}
|
}
|
||||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
(self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||||
}
|
}
|
||||||
if sectionIdentifier == .statuses {
|
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
default:
|
||||||
}
|
break
|
||||||
// we don't use the readable content inset here, because it insets the entire cell, rather than just the content
|
|
||||||
// so the cell backgrounds not being full width looks weird
|
|
||||||
return .list(using: config, layoutEnvironment: environment)
|
|
||||||
}
|
}
|
||||||
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
|
// we don't use the readable content inset here, because it insets the entire cell, rather than just the content
|
||||||
|
// so the cell backgrounds not being full width looks weird
|
||||||
|
return section
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
@ -113,10 +97,11 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
searchCancellable = searchSubject
|
_ = searchSubject
|
||||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||||
.map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
.sink { [unowned self] in self.performSearch(query: $0) }
|
.filter { $0 != self.currentQuery }
|
||||||
|
.sink(receiveValue: performSearch(query:))
|
||||||
|
|
||||||
userActivity = UserActivityManager.searchActivity(query: nil, accountID: mastodonController.accountInfo!.id)
|
userActivity = UserActivityManager.searchActivity(query: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
|
|
||||||
|
@ -130,9 +115,6 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
config.text = section.displayName
|
config.text = section.displayName
|
||||||
supplementaryView.contentConfiguration = config
|
supplementaryView.contentConfiguration = config
|
||||||
}
|
}
|
||||||
let tokenSuggestionCell = UICollectionView.CellRegistration<SearchTokenSuggestionCollectionViewCell, String> { cell, indexPath, itemIdentifier in
|
|
||||||
cell.setText(itemIdentifier)
|
|
||||||
}
|
|
||||||
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||||
cell.indicator.startAnimating()
|
cell.indicator.startAnimating()
|
||||||
}
|
}
|
||||||
|
@ -150,8 +132,6 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
let cell: UICollectionViewCell
|
let cell: UICollectionViewCell
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case .tokenSuggestion(let value):
|
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: tokenSuggestionCell, for: indexPath, item: value)
|
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
|
||||||
case .account(let accountID):
|
case .account(let accountID):
|
||||||
|
@ -192,45 +172,6 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
return super.targetViewController(forAction: action, sender: sender)
|
return super.targetViewController(forAction: action, sender: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTokenSuggestions(_ suggestions: [(SearchOperatorType, [String])], animated: Bool) {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
var prev: Section?
|
|
||||||
for (op, values) in suggestions {
|
|
||||||
let section = Section.tokenSuggestions(op)
|
|
||||||
if values.isEmpty {
|
|
||||||
if snapshot.sectionIdentifiers.contains(section) {
|
|
||||||
snapshot.deleteSections([section])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !snapshot.sectionIdentifiers.contains(section) {
|
|
||||||
if let prev {
|
|
||||||
snapshot.insertSections([section], afterSection: prev)
|
|
||||||
} else if let first = snapshot.sectionIdentifiers.first {
|
|
||||||
snapshot.insertSections([section], beforeSection: first)
|
|
||||||
} else {
|
|
||||||
snapshot.appendSections([section])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: section))
|
|
||||||
}
|
|
||||||
snapshot.appendItems(values.map { .tokenSuggestion($0) }, toSection: section)
|
|
||||||
prev = section
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeResults() {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
removeResults(from: &snapshot)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeResults(from snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
|
||||||
snapshot.deleteSections([Section.loadingIndicator, .accounts, .hashtags, .statuses].filter { snapshot.sectionIdentifiers.contains($0) })
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadResults(from source: SearchResultsViewController) {
|
func loadResults(from source: SearchResultsViewController) {
|
||||||
currentQuery = source.currentQuery
|
currentQuery = source.currentQuery
|
||||||
if let sourceDataSource = source.dataSource {
|
if let sourceDataSource = source.dataSource {
|
||||||
|
@ -239,18 +180,16 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func performSearch(query: String?) {
|
func performSearch(query: String?) {
|
||||||
guard isViewLoaded,
|
guard isViewLoaded else {
|
||||||
query != currentQuery else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let query = query, !query.isEmpty else {
|
guard let query = query, !query.isEmpty else {
|
||||||
removeResults()
|
self.dataSource.apply(NSDiffableDataSourceSnapshot<Section, Item>())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.currentQuery = query
|
self.currentQuery = query
|
||||||
|
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
removeResults(from: &snapshot)
|
|
||||||
snapshot.appendSections([.loadingIndicator])
|
snapshot.appendSections([.loadingIndicator])
|
||||||
snapshot.appendItems([.loadingIndicator])
|
snapshot.appendItems([.loadingIndicator])
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
|
@ -270,8 +209,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showSearchResults(_ results: SearchResults) {
|
private func showSearchResults(_ results: SearchResults) {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.deleteSections([.loadingIndicator])
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
||||||
let resultTypes = self.scope.resultTypes
|
let resultTypes = self.scope.resultTypes
|
||||||
|
@ -366,8 +304,7 @@ extension SearchResultsViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController {
|
extension SearchResultsViewController {
|
||||||
enum Section: Hashable {
|
enum Section: CaseIterable {
|
||||||
case tokenSuggestions(SearchOperatorType)
|
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
case accounts
|
case accounts
|
||||||
case hashtags
|
case hashtags
|
||||||
|
@ -375,8 +312,6 @@ extension SearchResultsViewController {
|
||||||
|
|
||||||
var displayName: String? {
|
var displayName: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .tokenSuggestions:
|
|
||||||
return nil
|
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
return nil
|
return nil
|
||||||
case .accounts:
|
case .accounts:
|
||||||
|
@ -389,7 +324,6 @@ extension SearchResultsViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tokenSuggestion(String)
|
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
case account(String)
|
case account(String)
|
||||||
case hashtag(Hashtag)
|
case hashtag(Hashtag)
|
||||||
|
@ -397,9 +331,6 @@ extension SearchResultsViewController {
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .tokenSuggestion(value):
|
|
||||||
hasher.combine("tokenSuggestion")
|
|
||||||
hasher.combine(value)
|
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
hasher.combine("loadingIndicator")
|
hasher.combine("loadingIndicator")
|
||||||
case let .account(id):
|
case let .account(id):
|
||||||
|
@ -416,8 +347,6 @@ extension SearchResultsViewController {
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.tokenSuggestion(let a), .tokenSuggestion(let b)):
|
|
||||||
return a == b
|
|
||||||
case (.loadingIndicator, .loadingIndicator):
|
case (.loadingIndicator, .loadingIndicator):
|
||||||
return true
|
return true
|
||||||
case (.account(let a), .account(let b)):
|
case (.account(let a), .account(let b)):
|
||||||
|
@ -447,12 +376,6 @@ extension SearchResultsViewController: UICollectionViewDelegate {
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case nil, .loadingIndicator:
|
case nil, .loadingIndicator:
|
||||||
return
|
return
|
||||||
case .tokenSuggestion(let value):
|
|
||||||
guard case .tokenSuggestions(let op) = dataSource.sectionIdentifier(for: indexPath.section) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tokenHandler?(value, op)
|
|
||||||
collectionView.deselectItem(at: indexPath, animated: true)
|
|
||||||
case let .account(id):
|
case let .account(id):
|
||||||
if let delegate {
|
if let delegate {
|
||||||
delegate.selectedSearchResult(account: id)
|
delegate.selectedSearchResult(account: id)
|
||||||
|
@ -480,7 +403,7 @@ extension SearchResultsViewController: UICollectionViewDelegate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch item {
|
switch item {
|
||||||
case .loadingIndicator, .tokenSuggestion(_):
|
case .loadingIndicator:
|
||||||
return nil
|
return nil
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
|
@ -513,7 +436,7 @@ extension SearchResultsViewController: UICollectionViewDragDelegate {
|
||||||
let url: URL
|
let url: URL
|
||||||
let activity: NSUserActivity
|
let activity: NSUserActivity
|
||||||
switch item {
|
switch item {
|
||||||
case .loadingIndicator, .tokenSuggestion(_):
|
case .loadingIndicator:
|
||||||
return []
|
return []
|
||||||
case .account(let id):
|
case .account(let id):
|
||||||
guard let account = mastodonController.persistentContainer.account(for: id) else {
|
guard let account = mastodonController.persistentContainer.account(for: id) else {
|
||||||
|
@ -541,29 +464,18 @@ extension SearchResultsViewController: UICollectionViewDragDelegate {
|
||||||
|
|
||||||
extension SearchResultsViewController: UISearchResultsUpdating {
|
extension SearchResultsViewController: UISearchResultsUpdating {
|
||||||
func updateSearchResults(for searchController: UISearchController) {
|
func updateSearchResults(for searchController: UISearchController) {
|
||||||
searchSubject.send(searchController.searchBar.searchQueryWithOperators)
|
searchSubject.send(searchController.searchBar.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController: UISearchBarDelegate {
|
extension SearchResultsViewController: UISearchBarDelegate {
|
||||||
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
let allSuggestionSections = snapshot.sectionIdentifiers.filter {
|
|
||||||
if case .tokenSuggestions(_) = $0 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snapshot.deleteSections(allSuggestionSections)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
|
||||||
|
|
||||||
// perform a search immedaitely when the search button is pressed
|
// perform a search immedaitely when the search button is pressed
|
||||||
performSearch(query: searchBar.searchQueryWithOperators)
|
performSearch(query: searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||||
let newQuery = searchBar.searchQueryWithOperators
|
let newQuery = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let newScope = Scope.allCases[selectedScope]
|
let newScope = Scope.allCases[selectedScope]
|
||||||
if self.scope == .all && currentQuery == newQuery {
|
if self.scope == .all && currentQuery == newQuery {
|
||||||
self.scope = newScope
|
self.scope = newScope
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
//
|
|
||||||
// SearchTokenSuggestionCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/1/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class SearchTokenSuggestionCollectionViewCell: UICollectionViewCell {
|
|
||||||
private let label = UILabel()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
label.textColor = .tintColor
|
|
||||||
|
|
||||||
contentView.backgroundColor = .tintColor.withAlphaComponent(0.2)
|
|
||||||
contentView.layer.masksToBounds = true
|
|
||||||
contentView.layer.cornerRadius = 6
|
|
||||||
contentView.layer.cornerCurve = .continuous
|
|
||||||
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(label)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
|
|
||||||
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
|
|
||||||
label.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
||||||
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setText(_ text: String) {
|
|
||||||
label.text = text
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -60,7 +60,6 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
||||||
let section: NSCollectionLayoutSection
|
|
||||||
switch dataSource.sectionIdentifier(for: sectionIndex)! {
|
switch dataSource.sectionIdentifier(for: sectionIndex)! {
|
||||||
case .status:
|
case .status:
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
|
@ -72,12 +71,14 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
|
||||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||||
}
|
}
|
||||||
section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
|
return section
|
||||||
case .accounts:
|
case .accounts:
|
||||||
section = NSCollectionLayoutSection.list(using: accountsConfig, layoutEnvironment: environment)
|
return NSCollectionLayoutSection.list(using: accountsConfig, layoutEnvironment: environment)
|
||||||
}
|
}
|
||||||
section.readableContentInset(in: environment)
|
|
||||||
return section
|
|
||||||
}
|
}
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
|
|
@ -54,7 +54,9 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -42,8 +42,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
private(set) var collectionView: UICollectionView!
|
private(set) var collectionView: UICollectionView!
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
|
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var userActivityNeedsUpdate = PassthroughSubject<Void, Never>()
|
private var userActivityNeedsUpdate = PassthroughSubject<Void, Never>()
|
||||||
// the last time this VC disappeared or the scene was backgrounded while it was active, used to decide if we want to check for present when reappearing
|
// the last time this VC disappeared or the scene was backgrounded while it was active, used to decide if we want to check for present when reappearing
|
||||||
|
@ -109,10 +107,12 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
// Use the sectionProvider closure, because the content inset depends on the environment.
|
// just setting layout.configuration.contentInsetsReference doesn't work with UICollectionViewCompositionalLayout.list
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
@ -1317,11 +1317,6 @@ extension TimelineViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
userActivityNeedsUpdate.send()
|
userActivityNeedsUpdate.send()
|
||||||
|
|
||||||
if reconfigureVisibleItemsOnEndDecelerating {
|
|
||||||
reconfigureVisibleItemsOnEndDecelerating = false
|
|
||||||
reconfigureVisibleCells()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
|
|
@ -21,8 +21,6 @@ protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeCon
|
||||||
|
|
||||||
var collectionView: UICollectionView! { get }
|
var collectionView: UICollectionView! { get }
|
||||||
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
|
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
|
||||||
|
|
||||||
var reconfigureVisibleItemsOnEndDecelerating: Bool { get set }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol TimelineLikeCollectionViewSection: Hashable, Sendable {
|
protocol TimelineLikeCollectionViewSection: Hashable, Sendable {
|
||||||
|
@ -127,18 +125,6 @@ extension TimelineLikeCollectionViewController {
|
||||||
var config: ToastConfiguration
|
var config: ToastConfiguration
|
||||||
if let error = error as? Self.Error,
|
if let error = error as? Self.Error,
|
||||||
error == .allCaughtUp {
|
error == .allCaughtUp {
|
||||||
// Reconfigure visible items to update timestamps.
|
|
||||||
#if targetEnvironment(macCatalyst)
|
|
||||||
let isRefreshing = false
|
|
||||||
#else
|
|
||||||
let isRefreshing = collectionView.refreshControl?.isRefreshing ?? false
|
|
||||||
#endif
|
|
||||||
if isRefreshing {
|
|
||||||
reconfigureVisibleItemsOnEndDecelerating = true
|
|
||||||
} else {
|
|
||||||
reconfigureVisibleCells()
|
|
||||||
}
|
|
||||||
|
|
||||||
config = ToastConfiguration(title: "You're all caught up")
|
config = ToastConfiguration(title: "You're all caught up")
|
||||||
config.edge = .top
|
config.edge = .top
|
||||||
config.dismissAutomaticallyAfter = 2
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
@ -218,16 +204,6 @@ extension TimelineLikeCollectionViewController {
|
||||||
await task.value
|
await task.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func reconfigureVisibleCells() {
|
|
||||||
let items = collectionView.indexPathsForVisibleItems.compactMap { dataSource.itemIdentifier(for: $0) }
|
|
||||||
if !items.isEmpty {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.reconfigureItems(items)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerTimelineLikeCells() {
|
func registerTimelineLikeCells() {
|
||||||
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
|
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
|
||||||
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
|
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NS_INLINE NSException * _Nullable catchNSException(void(^_Nonnull tryBlock)(void)) {
|
NS_INLINE NSException * _Nullable catchNSException(void(^_Nonnull tryBlock)(void)) {
|
||||||
@try {
|
@try {
|
||||||
|
@ -14,8 +13,3 @@ NS_INLINE NSException * _Nullable catchNSException(void(^_Nonnull tryBlock)(void
|
||||||
}
|
}
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define this private method so we can override it from MultiColumnCollectionViewLayout.
|
|
||||||
@interface UICollectionViewLayout (Tusker_Hacks)
|
|
||||||
-(UICollectionViewLayoutInvalidationContext *)_invalidationContextForUpdatedLayoutMargins:(UIEdgeInsets)newMargins;
|
|
||||||
@end
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
//
|
|
||||||
// Version.xcconfig
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/1/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
// Configuration settings file format documentation can be found at:
|
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
|
||||||
|
|
||||||
MARKETING_VERSION = 2023.8
|
|
||||||
CURRENT_PROJECT_VERSION = 104
|
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Release=
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Dist=
|
|
Loading…
Reference in New Issue