Compare commits

...

9 Commits

Author SHA1 Message Date
Shadowfacts cc10a13785 TextKit 2, baby 2022-06-29 00:12:45 -07:00
Shadowfacts f9c3ad5921 Bring back interactive keyboard dismissal on compose screen 2022-06-28 17:30:04 -07:00
Shadowfacts 0960699699 Fix building for iOS 14 2022-06-28 17:29:46 -07:00
Shadowfacts c6e06fe9f3 Use SwiftUI for sheet presentation detents on iOS 16 2022-06-28 17:29:46 -07:00
Shadowfacts 10f6a68065 Use new-style self-sizing cells on iOS 16 2022-06-28 17:29:46 -07:00
Shadowfacts 037b717e60 Include filename extension for attachments
Fixes posting attachments on pleroma resulting in them served as
application/octet-stream, even though we're sending the mime type as well
2022-06-28 17:29:46 -07:00
Shadowfacts 9fa352d4f8 Fix retain cycle in DiffableTimelineLikeTableViewController 2022-06-28 17:29:46 -07:00
Shadowfacts 73345bb927 Always used stacked search field in instance selector 2022-06-28 17:29:46 -07:00
Shadowfacts f5385b0a1d Use context menu for filter/sort on profile directory 2022-06-28 17:29:46 -07:00
22 changed files with 280 additions and 225 deletions

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum DirectoryOrder: String { public enum DirectoryOrder: String, CaseIterable {
case active case active
case new case new
} }

View File

@ -18,7 +18,6 @@
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */; };
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */; }; D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */; };
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */; }; D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */; };
@ -283,6 +282,7 @@
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; }; D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; }; D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */; };
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; }; D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; }; D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; }; D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
@ -366,7 +366,6 @@
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryFilterView.swift; sourceTree = "<group>"; };
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; }; D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = "<group>"; }; D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = "<group>"; };
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = "<group>"; }; D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = "<group>"; };
@ -635,6 +634,7 @@
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; }; D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; }; D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAction+Subtitle.swift"; sourceTree = "<group>"; };
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInTusker.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; }; D6E343AA265AAD6B00C4AA01 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; }; D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; };
@ -806,7 +806,6 @@
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */, D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */,
); );
path = Explore; path = Explore;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1124,6 +1123,7 @@
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */, D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
D62E9984279CA23900C26176 /* URLSession+Development.swift */, D62E9984279CA23900C26176 /* URLSession+Development.swift */,
D6E1EEF3285443EF00D20549 /* UIAction+Subtitle.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1744,7 +1744,6 @@
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */, D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */, D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */,
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */, D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
@ -1765,6 +1764,7 @@
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */, D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D6E1EEF4285443EF00D20549 /* UIAction+Subtitle.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
@ -2205,8 +2205,7 @@
CURRENT_PROJECT_VERSION = 31; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.3;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -2236,8 +2235,7 @@
CURRENT_PROJECT_VERSION = 31; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1; IPHONEOS_DEPLOYMENT_TARGET = 14.3;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@ -0,0 +1,19 @@
//
// UIAction+Subtitle.swift
// Tusker
//
// Created by Shadowfacts on 6/10/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
extension UIAction {
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
if #available(iOS 15.0, *) {
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
} else {
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
}
}
}

View File

@ -48,13 +48,13 @@ enum CompositionAttachmentData {
} }
} }
func getData(completion: @escaping (Result<(Data, String), Error>) -> Void) { func getData(completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
switch self { switch self {
case let .image(image): case let .image(image):
// Export as JPEG instead of PNG, otherweise photos straight from the camera are too large // Export as JPEG instead of PNG, otherweise photos straight from the camera are too large
// for Mastodon in its default configuration (max of 10MB). // for Mastodon in its default configuration (max of 10MB).
// The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future. // The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
completion(.success((image.jpegData(compressionQuality: 0.8)!, "image/jpeg"))) completion(.success((image.jpegData(compressionQuality: 0.8)!, .jpeg)))
case let .asset(asset): case let .asset(asset):
if asset.mediaType == .image { if asset.mediaType == .image {
let options = PHImageRequestOptions() let options = PHImageRequestOptions()
@ -68,19 +68,19 @@ enum CompositionAttachmentData {
return return
} }
let mimeType: String let utType: UTType
if dataUTI == "public.heic" { if dataUTI == "public.heic" {
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG // neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
let image = CIImage(data: data)! let image = CIImage(data: data)!
let context = CIContext() let context = CIContext()
let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)! let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])! data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
mimeType = "image/jpeg" utType = .jpeg
} else { } else {
mimeType = UTType(dataUTI)!.preferredMIMEType! utType = UTType(dataUTI)!
} }
completion(.success((data, mimeType))) completion(.success((data, utType)))
} }
} else if asset.mediaType == .video { } else if asset.mediaType == .video {
let options = PHVideoRequestOptions() let options = PHVideoRequestOptions()
@ -109,11 +109,11 @@ enum CompositionAttachmentData {
case let .drawing(drawing): case let .drawing(drawing):
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1) let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
completion(.success((image.pngData()!, "image/png"))) completion(.success((image.pngData()!, .png)))
} }
} }
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, String), Error>) -> Void) { private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
session.outputFileType = .mp4 session.outputFileType = .mp4
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4") session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
session.exportAsynchronously { session.exportAsynchronously {
@ -123,7 +123,7 @@ enum CompositionAttachmentData {
} }
do { do {
let data = try Data(contentsOf: session.outputURL!) let data = try Data(contentsOf: session.outputURL!)
completion(.success((data, "video/mp4"))) completion(.success((data, .mpeg4Movie)))
} catch { } catch {
completion(.failure(.videoExport(error))) completion(.failure(.videoExport(error)))
} }

View File

@ -154,6 +154,7 @@ extension ComposeAttachmentRow {
} }
private extension View { private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder @ViewBuilder
func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View { func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {

View File

@ -50,7 +50,7 @@ struct ComposeAttachmentsList: View {
.disabled(!canAddAttachment) .disabled(!canAddAttachment)
.foregroundColor(.blue) .foregroundColor(.blue)
.frame(height: cellHeight / 2) .frame(height: cellHeight / 2)
.popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover) .sheetOrPopover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
Button(action: self.createDrawing) { Button(action: self.createDrawing) {
@ -134,14 +134,21 @@ struct ComposeAttachmentsList: View {
private func assetPickerPopover() -> some View { private func assetPickerPopover() -> some View {
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate) ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
.onDisappear { .onDisappear {
// on iPadOS 16, this is necessary to dismiss the popover when collapsing from regular -> compact size class
// otherwise, the popover isn't visible but it's still "presented", so the sheet can't be shown
self.isShowingAssetPickerPopover = false self.isShowingAssetPickerPopover = false
} }
// on iPadOS 16, this is necessary to show the dark color in the popover arrow
.background(Color(.systemBackground))
.environment(\.colorScheme, .dark) .environment(\.colorScheme, .dark)
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
.withSheetDetentsIfAvailable()
} }
private func addAttachment() { private func addAttachment() {
if horizontalSizeClass == .regular { if #available(iOS 16.0, *) {
isShowingAssetPickerPopover = true
} else if horizontalSizeClass == .regular {
isShowingAssetPickerPopover = true isShowingAssetPickerPopover = true
} else { } else {
uiState.delegate?.presentAssetPickerSheet() uiState.delegate?.presentAssetPickerSheet()
@ -184,6 +191,7 @@ struct ComposeAttachmentsList: View {
} }
fileprivate extension View { fileprivate extension View {
@available(iOS, obsoleted: 15.0)
@ViewBuilder @ViewBuilder
func onDragWithPreviewIfAvailable<V>(_ data: @escaping () -> NSItemProvider, preview: () -> V) -> some View where V : View { func onDragWithPreviewIfAvailable<V>(_ data: @escaping () -> NSItemProvider, preview: () -> V) -> some View where V : View {
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
@ -192,6 +200,45 @@ fileprivate extension View {
self.onDrag(data) self.onDrag(data)
} }
} }
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {
if #available(iOS 16.0, *) {
self.modifier(SheetOrPopover(isPresented: isPresented, view: content))
} else {
self.popover(isPresented: isPresented, content: content)
}
}
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func withSheetDetentsIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
} else {
self
}
}
}
@available(iOS 16.0, *)
struct SheetOrPopover<V: View>: ViewModifier {
@Binding var isPresented: Bool
@ViewBuilder let view: () -> V
@Environment(\.horizontalSizeClass) var sizeClass
func body(content: Content) -> some View {
let _ = print("isPresented: \(isPresented)")
if sizeClass == .compact {
content.sheet(isPresented: $isPresented, content: view)
} else {
content.popover(isPresented: $isPresented, content: view)
}
}
} }
//struct ComposeAttachmentsList_Previews: PreviewProvider { //struct ComposeAttachmentsList_Previews: PreviewProvider {

View File

@ -471,13 +471,3 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
dismiss(animated: true) dismiss(animated: true)
} }
} }
fileprivate extension UIAction {
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
if #available(iOS 15.0, *) {
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
} else {
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
}
}
}

View File

@ -12,6 +12,7 @@ protocol ComposeUIStateDelegate: AnyObject {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get } var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
func dismissCompose(mode: ComposeUIState.DismissMode) func dismissCompose(mode: ComposeUIState.DismissMode)
// @available(iOS, obsoleted: 16.0)
func presentAssetPickerSheet() func presentAssetPickerSheet()
func presentComposeDrawing() func presentComposeDrawing()

View File

@ -89,7 +89,7 @@ struct ComposeView: View {
ScrollView(.vertical) { ScrollView(.vertical) {
mainStack(outerMinY: outer.frame(in: .global).minY) mainStack(outerMinY: outer.frame(in: .global).minY)
} }
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboardInteractivelyIfAvailable()
} }
if let poster = poster { if let poster = poster {
@ -251,6 +251,18 @@ struct ComposeView: View {
} }
} }
private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self.scrollDismissesKeyboard(.interactively)
} else {
self
}
}
}
//struct ComposeView_Previews: PreviewProvider { //struct ComposeView_Previews: PreviewProvider {
// static var previews: some View { // static var previews: some View {
// ComposeView() // ComposeView()

View File

@ -1,134 +0,0 @@
//
// ProfileDirectoryFilterView.swift
// Tusker
//
// Created by Shadowfacts on 2/7/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class ProfileDirectoryFilterView: UICollectionReusableView {
var onFilterChanged: ((Scope, DirectoryOrder) -> Void)?
private var scope: UISegmentedControl!
private var sort: UISegmentedControl!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
scope = UISegmentedControl(items: ["Instance", NSLocalizedString("Everywhere", comment: "everywhere profile directory scope")])
scope.selectedSegmentIndex = 0
scope.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
sort = UISegmentedControl(items: [
NSLocalizedString("Active", comment: "active profile directory sort"),
NSLocalizedString("New", comment: "new profile directory sort"),
])
sort.selectedSegmentIndex = 0
sort.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
let fromLabel = UILabel()
fromLabel.translatesAutoresizingMaskIntoConstraints = false
fromLabel.text = NSLocalizedString("From", comment: "profile directory scope label")
fromLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
fromLabel.textAlignment = .right
let sortLabel = UILabel()
sortLabel.translatesAutoresizingMaskIntoConstraints = false
sortLabel.text = NSLocalizedString("Sort By", comment: "profile directory sort label")
sortLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
sortLabel.textAlignment = .right
let labelContainer = UIView()
labelContainer.addSubview(sortLabel)
labelContainer.addSubview(fromLabel)
let controlStack = UIStackView(arrangedSubviews: [sort, scope])
controlStack.axis = .vertical
controlStack.spacing = 8
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: .label)
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
blurView.contentView.addSubview(vibrancyView)
let filterStack = UIStackView(arrangedSubviews: [
labelContainer,
controlStack,
])
filterStack.axis = .horizontal
filterStack.spacing = 8
filterStack.translatesAutoresizingMaskIntoConstraints = false
vibrancyView.contentView.addSubview(filterStack)
let separator = UIView()
separator.backgroundColor = .separator
separator.translatesAutoresizingMaskIntoConstraints = false
addSubview(separator)
NSLayoutConstraint.activate([
fromLabel.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor),
fromLabel.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor),
fromLabel.centerYAnchor.constraint(equalTo: scope.centerYAnchor),
sortLabel.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor),
sortLabel.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor),
sortLabel.centerYAnchor.constraint(equalTo: sort.centerYAnchor),
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurView.topAnchor.constraint(equalTo: topAnchor),
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
vibrancyView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
vibrancyView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
vibrancyView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
vibrancyView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
filterStack.leadingAnchor.constraint(equalToSystemSpacingAfter: vibrancyView.contentView.leadingAnchor, multiplier: 1),
vibrancyView.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: filterStack.trailingAnchor, multiplier: 1),
filterStack.topAnchor.constraint(equalToSystemSpacingBelow: vibrancyView.contentView.topAnchor, multiplier: 1),
vibrancyView.contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: filterStack.bottomAnchor, multiplier: 1),
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
separator.bottomAnchor.constraint(equalTo: bottomAnchor),
separator.heightAnchor.constraint(equalToConstant: 0.5),
])
}
func updateUI(mastodonController: MastodonController) {
scope.setTitle(mastodonController.accountInfo!.instanceURL.host!, forSegmentAt: 0)
}
@objc private func filterChanged() {
let scope = Scope(rawValue: self.scope.selectedSegmentIndex)!
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
onFilterChanged?(scope, order)
}
}
extension ProfileDirectoryFilterView {
enum Scope: Int, Equatable {
case instance, everywhere
}
}

View File

@ -16,6 +16,9 @@ class ProfileDirectoryViewController: UIViewController {
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var scope: Scope = .everywhere
private var order: DirectoryOrder = .active
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -31,14 +34,10 @@ class ProfileDirectoryViewController: UIViewController {
title = NSLocalizedString("Profile Directory", comment: "profile directory title") title = NSLocalizedString("Profile Directory", comment: "profile directory title")
let configuration = UICollectionViewCompositionalLayoutConfiguration() // todo: it would be nice if there were a better "filter" icon
configuration.boundarySupplementaryItems = [ navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "scope"), menu: nil)
NSCollectionLayoutBoundarySupplementaryItem( updateFilterMenu()
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100)),
elementKind: "filter",
alignment: .top
)
]
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
let itemHeight = NSCollectionLayoutDimension.absolute(200) let itemHeight = NSCollectionLayoutDimension.absolute(200)
let itemWidth: NSCollectionLayoutDimension let itemWidth: NSCollectionLayoutDimension
@ -60,19 +59,18 @@ class ProfileDirectoryViewController: UIViewController {
section.interGroupSpacing = 16 section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
return section return section
}, configuration: configuration) })
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .secondarySystemBackground collectionView.backgroundColor = .secondarySystemBackground
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell") collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
collectionView.register(ProfileDirectoryFilterView.self, forSupplementaryViewOfKind: "filter", withReuseIdentifier: "filter")
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
view.addSubview(collectionView) view.addSubview(collectionView)
dataSource = createDataSource() dataSource = createDataSource()
updateProfiles(local: true, order: .active) updateProfiles()
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -82,26 +80,44 @@ class ProfileDirectoryViewController: UIViewController {
cell.updateUI(account: account) cell.updateUI(account: account)
return cell return cell
} }
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
guard elementKind == "filter" else {
return nil
}
let filterView = collectionView.dequeueReusableSupplementaryView(ofKind: "filter", withReuseIdentifier: "filter", for: indexPath) as! ProfileDirectoryFilterView
filterView.updateUI(mastodonController: self.mastodonController)
filterView.onFilterChanged = { [weak self] (scope, order) in
guard let self = self else { return }
self.dataSource.apply(.init())
self.updateProfiles(local: scope == .instance, order: order)
}
return filterView
}
return dataSource return dataSource
} }
private func updateProfiles(local: Bool, order: DirectoryOrder) { private func updateFilterMenu() {
let scopeMenu = UIMenu(options: .displayInline, children: [
UIAction(title: "Everywhere", subtitle: "Users from the whole network", image: UIImage(systemName: "globe"), state: scope == .everywhere ? .on : .off, handler: { [unowned self] _ in
self.scope = .everywhere
self.updateFilterMenu()
self.updateProfiles()
}),
UIAction(title: mastodonController.accountInfo!.instanceURL.host!, subtitle: "Only users from your instance", image: UIImage(systemName: "house"), state: scope == .instance ? .on : .off, handler: { [unowned self] _ in
self.scope = .instance
self.updateFilterMenu()
self.updateProfiles()
}),
])
let orderMenu = UIMenu(options: .displayInline, children: DirectoryOrder.allCases.map { order in
UIAction(title: order.title, subtitle: order.subtitle, image: nil, state: self.order == order ? .on : .off) { [unowned self] _ in
self.order = order
self.updateFilterMenu()
self.updateProfiles()
}
})
navigationItem.rightBarButtonItem!.menu = UIMenu(children: [
scopeMenu,
orderMenu,
])
}
private func updateProfiles() {
let scope = self.scope
let order = self.order
let local = scope == .everywhere
let request = Client.getFeaturedProfiles(local: local, order: order) let request = Client.getFeaturedProfiles(local: local, order: order)
mastodonController.run(request) { (response) in mastodonController.run(request) { (response) in
guard case let .success(accounts, _) = response else { guard case let .success(accounts, _) = response,
self.scope == scope,
self.order == order else {
return return
} }
@ -188,3 +204,28 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
return [UIDragItem(itemProvider: provider)] return [UIDragItem(itemProvider: provider)]
} }
} }
extension ProfileDirectoryViewController {
enum Scope: CaseIterable {
case instance, everywhere
}
}
private extension DirectoryOrder {
var title: String {
switch self {
case .active:
return "Active"
case .new:
return "New"
}
}
var subtitle: String {
switch self {
case .active:
return "Users who have posted lately"
case .new:
return "Recently joined users"
}
}
}

View File

@ -17,7 +17,7 @@ protocol LargeImageContentView: UIView {
func grayscaleStateChanged() func grayscaleStateChanged()
} }
class LargeImageImageContentView: UIImageView, LargeImageContentView, ImageAnalysisInteractionDelegate { class LargeImageImageContentView: UIImageView, LargeImageContentView {
@available(iOS 16.0, *) @available(iOS 16.0, *)
private static let analyzer = ImageAnalyzer() private static let analyzer = ImageAnalyzer()
@ -76,7 +76,10 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView, ImageAnaly
self.image = image self.image = image
} }
} }
}
@available(iOS 16.0, *)
extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? { func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? {
return owner return owner
} }

View File

@ -255,8 +255,11 @@ extension NotificationsTableViewController: MenuActionProvider {
extension NotificationsTableViewController: StatusTableViewCellDelegate { extension NotificationsTableViewController: StatusTableViewCellDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
if #available(iOS 16.0, *) {
} else {
cellHeightChanged() cellHeightChanged()
} }
}
} }
extension NotificationsTableViewController: UITableViewDataSourcePrefetching { extension NotificationsTableViewController: UITableViewDataSourcePrefetching {

View File

@ -81,6 +81,9 @@ class InstanceSelectorTableViewController: UITableViewController {
searchController.searchBar.searchTextField.autocapitalizationType = .none searchController.searchBar.searchTextField.autocapitalizationType = .none
navigationItem.searchController = searchController navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
}
definesPresentationContext = true definesPresentationContext = true
urlHandler = urlCheckerSubject urlHandler = urlCheckerSubject

View File

@ -265,8 +265,11 @@ extension ProfileStatusesViewController: TuskerNavigationDelegate {
extension ProfileStatusesViewController: StatusTableViewCellDelegate { extension ProfileStatusesViewController: StatusTableViewCellDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
if #available(iOS 16.0, *) {
} else {
cellHeightChanged() cellHeightChanged()
} }
}
} }
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching { extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {

View File

@ -290,8 +290,11 @@ extension TimelineTableViewController: TuskerNavigationDelegate {
extension TimelineTableViewController: StatusTableViewCellDelegate { extension TimelineTableViewController: StatusTableViewCellDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
if #available(iOS 16.0, *) {
} else {
cellHeightChanged() cellHeightChanged()
} }
}
} }
extension TimelineTableViewController: MenuActionProvider { extension TimelineTableViewController: MenuActionProvider {

View File

@ -34,7 +34,9 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: self.cellProvider) dataSource = UITableViewDiffableDataSource(tableView: tableView) { [unowned self] (tableView, indexPath, item) in
self.cellProvider(tableView, indexPath, item)
}
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140
@ -161,6 +163,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
} }
} }
@available(iOS, deprecated: 16.0)
func cellHeightChanged() { func cellHeightChanged() {
// causes the table view to recalculate the cell heights // causes the table view to recalculate the cell heights
tableView.beginUpdates() tableView.beginUpdates()

View File

@ -20,19 +20,23 @@ class EnhancedNavigationViewController: UINavigationController {
override var viewControllers: [UIViewController] { override var viewControllers: [UIViewController] {
didSet { didSet {
poppedViewControllers = [] poppedViewControllers = []
if #available(iOS 16.0, *) {
// TODO: this for loop might not be necessary
for vc in viewControllers { for vc in viewControllers {
configureNavItem(vc.navigationItem) configureNavItem(vc.navigationItem)
} }
updateTopNavItemState() updateTopNavItemState()
} }
} }
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
self.interactivePushTransition = InteractivePushTransition(navigationController: self) self.interactivePushTransition = InteractivePushTransition(navigationController: self)
if let topViewController { if #available(iOS 16.0, *),
let topViewController {
configureNavItem(topViewController.navigationItem) configureNavItem(topViewController.navigationItem)
updateTopNavItemState() updateTopNavItemState()
} }
@ -42,7 +46,9 @@ class EnhancedNavigationViewController: UINavigationController {
let popped = performAfterAnimating(block: { let popped = performAfterAnimating(block: {
super.popViewController(animated: animated) super.popViewController(animated: animated)
}, after: { }, after: {
if #available(iOS 16.0, *) {
self.updateTopNavItemState() self.updateTopNavItemState()
}
}, animated: animated) }, animated: animated)
if let popped { if let popped {
poppedViewControllers.insert(popped, at: 0) poppedViewControllers.insert(popped, at: 0)
@ -54,7 +60,9 @@ class EnhancedNavigationViewController: UINavigationController {
let popped = performAfterAnimating(block: { let popped = performAfterAnimating(block: {
super.popToRootViewController(animated: animated) super.popToRootViewController(animated: animated)
}, after: { }, after: {
if #available(iOS 16.0, *) {
self.updateTopNavItemState() self.updateTopNavItemState()
}
}, animated: animated) }, animated: animated)
if let popped { if let popped {
poppedViewControllers = popped poppedViewControllers = popped
@ -66,7 +74,9 @@ class EnhancedNavigationViewController: UINavigationController {
let popped = performAfterAnimating(block: { let popped = performAfterAnimating(block: {
super.popToViewController(viewController, animated: animated) super.popToViewController(viewController, animated: animated)
}, after: { }, after: {
if #available(iOS 16.0, *) {
self.updateTopNavItemState() self.updateTopNavItemState()
}
}, animated: animated) }, animated: animated)
if let popped { if let popped {
poppedViewControllers.insert(contentsOf: popped, at: 0) poppedViewControllers.insert(contentsOf: popped, at: 0)
@ -81,12 +91,16 @@ class EnhancedNavigationViewController: UINavigationController {
self.poppedViewControllers = [] self.poppedViewControllers = []
} }
if #available(iOS 16.0, *) {
configureNavItem(viewController.navigationItem) configureNavItem(viewController.navigationItem)
}
super.pushViewController(viewController, animated: animated) super.pushViewController(viewController, animated: animated)
if #available(iOS 16.0, *) {
updateTopNavItemState() updateTopNavItemState()
} }
}
func pushPoppedViewController() { func pushPoppedViewController() {
guard !poppedViewControllers.isEmpty else { guard !poppedViewControllers.isEmpty else {
@ -115,7 +129,9 @@ class EnhancedNavigationViewController: UINavigationController {
pushViewController(target, animated: true) pushViewController(target, animated: true)
}, after: { }, after: {
self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1) self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1)
if #available(iOS 16.0, *) {
self.updateTopNavItemState() self.updateTopNavItemState()
}
}, animated: true) }, animated: true)
} }
@ -135,6 +151,7 @@ class EnhancedNavigationViewController: UINavigationController {
}) })
} }
@available(iOS 16.0, *)
private func configureNavItem(_ navItem: UINavigationItem) { private func configureNavItem(_ navItem: UINavigationItem) {
guard useBrowserStyleNavigation, guard useBrowserStyleNavigation,
UIDevice.current.userInterfaceIdiom != .phone else { UIDevice.current.userInterfaceIdiom != .phone else {
@ -183,6 +200,7 @@ class EnhancedNavigationViewController: UINavigationController {
] ]
} }
@available(iOS 16.0, *)
private func updateTopNavItemState() { private func updateTopNavItemState() {
guard useBrowserStyleNavigation, guard useBrowserStyleNavigation,
UIDevice.current.userInterfaceIdiom != .phone, UIDevice.current.userInterfaceIdiom != .phone,

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
import UniformTypeIdentifiers
class PostService: ObservableObject { class PostService: ObservableObject {
private let mastodonController: MastodonController private let mastodonController: MastodonController
@ -66,15 +67,15 @@ class PostService: ObservableObject {
attachments.reserveCapacity(draft.attachments.count) attachments.reserveCapacity(draft.attachments.count)
for (index, attachment) in draft.attachments.enumerated() { for (index, attachment) in draft.attachments.enumerated() {
let data: Data let data: Data
let mimeType: String let utType: UTType
do { do {
(data, mimeType) = try await getData(for: attachment) (data, utType) = try await getData(for: attachment)
currentStep += 1 currentStep += 1
} catch let error as CompositionAttachmentData.Error { } catch let error as CompositionAttachmentData.Error {
throw Error.attachmentData(index: index, cause: error) throw Error.attachmentData(index: index, cause: error)
} }
do { do {
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.attachmentDescription) let uploaded = try await uploadAttachment(data: data, utType: utType, description: attachment.attachmentDescription)
attachments.append(uploaded) attachments.append(uploaded)
currentStep += 1 currentStep += 1
} catch let error as Client.Error { } catch let error as Client.Error {
@ -84,7 +85,7 @@ class PostService: ObservableObject {
return attachments return attachments
} }
private func getData(for attachment: CompositionAttachment) async throws -> (Data, String) { private func getData(for attachment: CompositionAttachment) async throws -> (Data, UTType) {
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
attachment.data.getData { result in attachment.data.getData { result in
switch result { switch result {
@ -97,8 +98,8 @@ class PostService: ObservableObject {
} }
} }
private func uploadAttachment(data: Data, mimeType: String, description: String?) async throws -> Attachment { private func uploadAttachment(data: Data, utType: UTType, description: String?) async throws -> Attachment {
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file") let formAttachment = FormAttachment(mimeType: utType.preferredMIMEType!, data: data, fileName: "file.\(utType.preferredFilenameExtension!)")
let req = Client.upload(attachment: formAttachment, description: description) let req = Client.upload(attachment: formAttachment, description: description)
return try await mastodonController.run(req).0 return try await mastodonController.run(req).0
} }

View File

@ -195,16 +195,39 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? { func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top) let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
if #available(iOS 16.0, *),
let textLayoutManager {
guard let fragment = textLayoutManager.textLayoutFragment(for: point),
let lineFragment = fragment.textLineFragments.first(where: { lineFragment in
lineFragment.typographicBounds.offsetBy(dx: fragment.layoutFragmentFrame.minX, dy: fragment.layoutFragmentFrame.minY).contains(point)
}) else {
return nil
}
let charIndex = lineFragment.characterIndex(for: point)
var range = NSRange()
guard let link = lineFragment.attributedString.attribute(.link, at: charIndex, longestEffectiveRange: &range, in: lineFragment.attributedString.fullRange) as? URL else {
return nil
}
// lineFragment.attributedString is the NSTextLayoutFragment's string, and so range is in its index space
// but we need to return a range in our whole attributedString's space, so convert it
let textLayoutFragmentStart = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: fragment.rangeInElement.location)
let rangeInSelf = NSRange(location: range.location + textLayoutFragmentStart, length: range.length)
return (link, rangeInSelf)
} else {
var partialFraction: CGFloat = 0 var partialFraction: CGFloat = 0
let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction) let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction)
if characterIndex < textStorage.length && partialFraction < 1 { guard characterIndex < textStorage.length && partialFraction < 1 else {
return nil
}
var range = NSRange() var range = NSRange()
if let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL { guard let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL else {
return nil
}
return (link, range) return (link, range)
} }
} }
return nil
}
func handleLinkTapped(url: URL, text: String) { func handleLinkTapped(url: URL, text: String) {
if let mention = getMention(for: url, text: text) { if let mention = getMention(for: url, text: text) {
@ -297,9 +320,26 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
// Determine the line rects that the link takes up in the coordinate space of this view. // Determine the line rects that the link takes up in the coordinate space of this view.
var rects = [CGRect]() var rects = [CGRect]()
if #available(iOS 16.0, *),
let textLayoutManager,
let contentManager = textLayoutManager.textContentManager {
// convert from NSRange to NSTextRange
// i have no idea under what circumstances any of these calls could fail
guard let startLoc = contentManager.location(contentManager.documentRange.location, offsetBy: range.location),
let endLoc = contentManager.location(startLoc, offsetBy: range.length),
let textRange = NSTextRange(location: startLoc, end: endLoc) else {
return nil
}
// .standard because i have no idea what the difference is
textLayoutManager.enumerateTextSegments(in: textRange, type: .standard, options: []) { range, rect, float, textContainer in
rects.append(rect)
return true
}
} else {
layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in
rects.append(rect) rects.append(rect)
} }
}
// Try to create a snapshot view of this view to disply as the preview. // Try to create a snapshot view of this view to disply as the preview.
// If a snapshot view cannot be created, we bail and use the system-provided preview. // If a snapshot view cannot be created, we bail and use the system-provided preview.

View File

@ -171,7 +171,6 @@ class ProfileHeaderView: UIView {
} }
private func updateRelationship() { private func updateRelationship() {
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else { let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
return return
@ -181,7 +180,6 @@ class ProfileHeaderView: UIView {
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
guard let mastodonController = mastodonController, guard let mastodonController = mastodonController,
// nil if prefs changed before own account is loaded // nil if prefs changed before own account is loaded
let accountID = accountID, let accountID = accountID,

View File

@ -12,6 +12,7 @@ import Combine
import AVKit import AVKit
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate, MenuActionProvider { protocol StatusTableViewCellDelegate: TuskerNavigationDelegate, MenuActionProvider {
// @available(iOS, obsoleted: 16.0)
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
} }
@ -332,8 +333,12 @@ class BaseStatusTableViewCell: UITableViewCell {
@IBAction func collapseButtonPressed() { @IBAction func collapseButtonPressed() {
setCollapsed(!collapsed, animated: true) setCollapsed(!collapsed, animated: true)
if #available(iOS 16.0, *) {
invalidateIntrinsicContentSize()
} else {
delegate?.statusCellCollapsedStateChanged(self) delegate?.statusCellCollapsedStateChanged(self)
} }
}
func setCollapsed(_ collapsed: Bool, animated: Bool) { func setCollapsed(_ collapsed: Bool, animated: Bool) {
self.collapsed = collapsed self.collapsed = collapsed