Compare commits
10 Commits
e4eff2d362
...
cb47443649
Author | SHA1 | Date |
---|---|---|
Shadowfacts | cb47443649 | |
Shadowfacts | 86862825f6 | |
Shadowfacts | e6f1968609 | |
Shadowfacts | 4c5da1b5a9 | |
Shadowfacts | e57ef210fd | |
Shadowfacts | dcdfe853e1 | |
Shadowfacts | 34e57c297b | |
Shadowfacts | 6c2c2e6ae7 | |
Shadowfacts | aae3bd0bba | |
Shadowfacts | 2b5d4681e3 |
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2023.8 (106)
|
||||||
|
Bugfixes:
|
||||||
|
- Fix being able to set post language to multiple/undefined
|
||||||
|
- iPadOS: Fix language picker button not having a pointer effect
|
||||||
|
- macOS: Fix Cmd+W sometimes closing the non-foreground window
|
||||||
|
|
||||||
## 2023.8 (105)
|
## 2023.8 (105)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Use server-set preference for default post visibility, language, and (on Hometown) local-only
|
- Use server-set preference for default post visibility, language, and (on Hometown) local-only
|
||||||
|
|
|
@ -111,7 +111,7 @@ class PostService: ObservableObject {
|
||||||
do {
|
do {
|
||||||
(data, utType) = try await getData(for: attachment)
|
(data, utType) = try await getData(for: attachment)
|
||||||
currentStep += 1
|
currentStep += 1
|
||||||
} catch let error as AttachmentData.Error {
|
} catch let error as DraftAttachment.ExportError {
|
||||||
throw Error.attachmentData(index: index, cause: error)
|
throw Error.attachmentData(index: index, cause: error)
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
@ -169,7 +169,7 @@ class PostService: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Error: Swift.Error, LocalizedError {
|
enum Error: Swift.Error, LocalizedError {
|
||||||
case attachmentData(index: Int, cause: AttachmentData.Error)
|
case attachmentData(index: Int, cause: DraftAttachment.ExportError)
|
||||||
case attachmentUpload(index: Int, cause: Client.Error)
|
case attachmentUpload(index: Int, cause: Client.Error)
|
||||||
case posting(Client.Error)
|
case posting(Client.Error)
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,11 @@ public final class ComposeController: ViewController {
|
||||||
public typealias ReplyContentView = (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView
|
public typealias ReplyContentView = (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView
|
||||||
public typealias EmojiImageView = (Emoji) -> AnyView
|
public typealias EmojiImageView = (Emoji) -> AnyView
|
||||||
|
|
||||||
@Published public private(set) var draft: Draft
|
@Published public private(set) var draft: Draft {
|
||||||
|
didSet {
|
||||||
|
assert(draft.managedObjectContext == DraftsPersistentContainer.shared.viewContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
@Published public var config: ComposeUIConfig
|
@Published public var config: ComposeUIConfig
|
||||||
@Published public var mastodonController: ComposeMastodonContext
|
@Published public var mastodonController: ComposeMastodonContext
|
||||||
let fetchAvatar: AvatarImageView.FetchAvatar
|
let fetchAvatar: AvatarImageView.FetchAvatar
|
||||||
|
@ -106,6 +110,7 @@ public final class ComposeController: ViewController {
|
||||||
emojiImageView: @escaping EmojiImageView
|
emojiImageView: @escaping EmojiImageView
|
||||||
) {
|
) {
|
||||||
self.draft = draft
|
self.draft = draft
|
||||||
|
assert(draft.managedObjectContext == DraftsPersistentContainer.shared.viewContext)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.fetchAvatar = fetchAvatar
|
self.fetchAvatar = fetchAvatar
|
||||||
|
|
|
@ -137,6 +137,8 @@ extension DraftAttachment {
|
||||||
//private let attachmentTypeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
//private let attachmentTypeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||||
|
|
||||||
private let imageType = UTType.image.identifier
|
private let imageType = UTType.image.identifier
|
||||||
|
private let heifType = UTType.heif.identifier
|
||||||
|
private let heicType = UTType.heic.identifier
|
||||||
private let jpegType = UTType.jpeg.identifier
|
private let jpegType = UTType.jpeg.identifier
|
||||||
private let pngType = UTType.png.identifier
|
private let pngType = UTType.png.identifier
|
||||||
private let mp4Type = UTType.mpeg4Movie.identifier
|
private let mp4Type = UTType.mpeg4Movie.identifier
|
||||||
|
@ -148,7 +150,7 @@ extension DraftAttachment: NSItemProviderReading {
|
||||||
// todo: is there a better way of handling movies than manually adding all possible UTI types?
|
// todo: is there a better way of handling movies than manually adding all possible UTI types?
|
||||||
// just using kUTTypeMovie doesn't work, because we need the actually type in order to get the file extension
|
// just using kUTTypeMovie doesn't work, because we need the actually type in order to get the file extension
|
||||||
// without the file extension, getting the thumbnail and exporting the video for attachment upload fails
|
// without the file extension, getting the thumbnail and exporting the video for attachment upload fails
|
||||||
[/*typeIdentifier, */ gifType, jpegType, pngType, imageType, mp4Type, quickTimeType]
|
[/*typeIdentifier, */ gifType, heifType, heicType, jpegType, pngType, imageType, mp4Type, quickTimeType]
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> DraftAttachment {
|
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> DraftAttachment {
|
||||||
|
@ -273,20 +275,13 @@ extension DraftAttachment {
|
||||||
var data = data
|
var data = data
|
||||||
var type = type
|
var type = type
|
||||||
|
|
||||||
if type != .png && type != .jpeg,
|
|
||||||
let image = UIImage(data: data) {
|
|
||||||
// The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
|
|
||||||
data = image.jpegData(compressionQuality: 0.8)!
|
|
||||||
type = .jpeg
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = CIImage(data: data)!
|
let image = CIImage(data: data)!
|
||||||
let needsColorSpaceConversion = features.needsWideColorGamutHack && image.colorSpace?.name != CGColorSpace.sRGB
|
let needsColorSpaceConversion = features.needsWideColorGamutHack && image.colorSpace?.name != CGColorSpace.sRGB
|
||||||
|
|
||||||
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
||||||
// they also do a bad job converting wide color gamut images (they seem to just drop the profile, letting the wide-gamut values be reinterprete as sRGB)
|
// they also do a bad job converting wide color gamut images (they seem to just drop the profile, letting the wide-gamut values be reinterprete as sRGB)
|
||||||
// if that changes in the future, we'll need to pass the InstanceFeatures in here somehow and gate the conversion
|
// if that changes in the future, we'll need to pass the InstanceFeatures in here somehow and gate the conversion
|
||||||
if needsColorSpaceConversion || type == .heic {
|
if needsColorSpaceConversion || type == .heic || type == .heif {
|
||||||
let context = CIContext()
|
let context = CIContext()
|
||||||
let colorSpace = needsColorSpaceConversion || image.colorSpace != nil ? CGColorSpace(name: CGColorSpace.sRGB)! : image.colorSpace!
|
let colorSpace = needsColorSpaceConversion || image.colorSpace != nil ? CGColorSpace(name: CGColorSpace.sRGB)! : image.colorSpace!
|
||||||
if type == .png {
|
if type == .png {
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
//
|
|
||||||
// AttachmentData.swift
|
|
||||||
// ComposeUI
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 1/1/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Photos
|
|
||||||
import UniformTypeIdentifiers
|
|
||||||
import PencilKit
|
|
||||||
import InstanceFeatures
|
|
||||||
|
|
||||||
enum AttachmentData {
|
|
||||||
case asset(PHAsset)
|
|
||||||
case image(Data, originalType: UTType)
|
|
||||||
case video(URL)
|
|
||||||
case drawing(PKDrawing)
|
|
||||||
case gif(Data)
|
|
||||||
|
|
||||||
var type: AttachmentType {
|
|
||||||
switch self {
|
|
||||||
case let .asset(asset):
|
|
||||||
return asset.attachmentType!
|
|
||||||
case .image(_, originalType: _):
|
|
||||||
return .image
|
|
||||||
case .video(_):
|
|
||||||
return .video
|
|
||||||
case .drawing(_):
|
|
||||||
return .image
|
|
||||||
case .gif(_):
|
|
||||||
return .image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isAsset: Bool {
|
|
||||||
switch self {
|
|
||||||
case .asset(_):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var canSaveToDraft: Bool {
|
|
||||||
switch self {
|
|
||||||
case .video(_):
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getData(features: InstanceFeatures, skipAllConversion: Bool = false, completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
|
|
||||||
switch self {
|
|
||||||
case let .image(originalData, originalType):
|
|
||||||
let data: Data
|
|
||||||
let type: UTType
|
|
||||||
switch originalType {
|
|
||||||
case .png, .jpeg:
|
|
||||||
data = originalData
|
|
||||||
type = originalType
|
|
||||||
default:
|
|
||||||
let image = UIImage(data: originalData)!
|
|
||||||
// The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
|
|
||||||
data = image.jpegData(compressionQuality: 0.8)!
|
|
||||||
type = .jpeg
|
|
||||||
}
|
|
||||||
let processed = processImageData(data, type: type, features: features, skipAllConversion: skipAllConversion)
|
|
||||||
completion(.success(processed))
|
|
||||||
case let .asset(asset):
|
|
||||||
if asset.mediaType == .image {
|
|
||||||
let options = PHImageRequestOptions()
|
|
||||||
options.version = .current
|
|
||||||
options.deliveryMode = .highQualityFormat
|
|
||||||
options.resizeMode = .none
|
|
||||||
options.isNetworkAccessAllowed = true
|
|
||||||
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in
|
|
||||||
guard let data = data, let dataUTI = dataUTI else {
|
|
||||||
completion(.failure(.missingData))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let processed = processImageData(data, type: UTType(dataUTI)!, features: features, skipAllConversion: skipAllConversion)
|
|
||||||
completion(.success(processed))
|
|
||||||
}
|
|
||||||
} else if asset.mediaType == .video {
|
|
||||||
let options = PHVideoRequestOptions()
|
|
||||||
options.deliveryMode = .automatic
|
|
||||||
options.isNetworkAccessAllowed = true
|
|
||||||
options.version = .current
|
|
||||||
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
|
||||||
if let exportSession = exportSession {
|
|
||||||
AttachmentData.exportVideoData(session: exportSession, completion: completion)
|
|
||||||
} else if let error = info?[PHImageErrorKey] as? Error {
|
|
||||||
completion(.failure(.videoExport(error)))
|
|
||||||
} else {
|
|
||||||
completion(.failure(.noVideoExportSession))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fatalError("assetType must be either image or video")
|
|
||||||
}
|
|
||||||
case let .video(url):
|
|
||||||
let asset = AVURLAsset(url: url)
|
|
||||||
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
|
||||||
completion(.failure(.noVideoExportSession))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
AttachmentData.exportVideoData(session: session, completion: completion)
|
|
||||||
|
|
||||||
case let .drawing(drawing):
|
|
||||||
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
|
|
||||||
completion(.success((image.pngData()!, .png)))
|
|
||||||
case let .gif(data):
|
|
||||||
completion(.success((data, .gif)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func processImageData(_ data: Data, type: UTType, features: InstanceFeatures, skipAllConversion: Bool) -> (Data, UTType) {
|
|
||||||
guard !skipAllConversion else {
|
|
||||||
return (data, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = data
|
|
||||||
var type = type
|
|
||||||
let image = CIImage(data: data)!
|
|
||||||
let needsColorSpaceConversion = features.needsWideColorGamutHack && image.colorSpace?.name != CGColorSpace.sRGB
|
|
||||||
|
|
||||||
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
|
||||||
// they also do a bad job converting wide color gamut images (they seem to just drop the profile, letting the wide-gamut values be reinterprete as sRGB)
|
|
||||||
// if that changes in the future, we'll need to pass the InstanceFeatures in here somehow and gate the conversion
|
|
||||||
if needsColorSpaceConversion || type == .heic {
|
|
||||||
let context = CIContext()
|
|
||||||
let colorSpace = needsColorSpaceConversion || image.colorSpace != nil ? CGColorSpace(name: CGColorSpace.sRGB)! : image.colorSpace!
|
|
||||||
if type == .png {
|
|
||||||
data = context.pngRepresentation(of: image, format: .ARGB8, colorSpace: colorSpace)!
|
|
||||||
} else {
|
|
||||||
data = context.jpegRepresentation(of: image, colorSpace: colorSpace)!
|
|
||||||
type = .jpeg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (data, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
|
|
||||||
session.outputFileType = .mp4
|
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
|
||||||
session.exportAsynchronously {
|
|
||||||
guard session.status == .completed else {
|
|
||||||
completion(.failure(.videoExport(session.error!)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: session.outputURL!)
|
|
||||||
completion(.success((data, .mpeg4Movie)))
|
|
||||||
} catch {
|
|
||||||
completion(.failure(.videoExport(error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AttachmentType {
|
|
||||||
case image, video
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Error: Swift.Error, LocalizedError {
|
|
||||||
case missingData
|
|
||||||
case videoExport(Swift.Error)
|
|
||||||
case noVideoExportSession
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
switch self {
|
|
||||||
case .missingData:
|
|
||||||
return "Missing Data"
|
|
||||||
case .videoExport(let error):
|
|
||||||
return "Exporting video: \(error)"
|
|
||||||
case .noVideoExportSession:
|
|
||||||
return "Couldn't create video export session"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PHAsset {
|
|
||||||
var attachmentType: AttachmentData.AttachmentType? {
|
|
||||||
switch self.mediaType {
|
|
||||||
case .image:
|
|
||||||
return .image
|
|
||||||
case .video:
|
|
||||||
return .video
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AttachmentData: Codable {
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
switch self {
|
|
||||||
case let .asset(asset):
|
|
||||||
try container.encode("asset", forKey: .type)
|
|
||||||
try container.encode(asset.localIdentifier, forKey: .assetIdentifier)
|
|
||||||
case let .image(originalData, originalType):
|
|
||||||
try container.encode("image", forKey: .type)
|
|
||||||
try container.encode(originalType, forKey: .imageType)
|
|
||||||
try container.encode(originalData, forKey: .imageData)
|
|
||||||
case .video(_):
|
|
||||||
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "video CompositionAttachments cannot be encoded"))
|
|
||||||
case let .drawing(drawing):
|
|
||||||
try container.encode("drawing", forKey: .type)
|
|
||||||
let drawingData = drawing.dataRepresentation()
|
|
||||||
try container.encode(drawingData, forKey: .drawing)
|
|
||||||
case .gif(_):
|
|
||||||
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "gif CompositionAttachments cannot be encoded"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
switch try container.decode(String.self, forKey: .type) {
|
|
||||||
case "asset":
|
|
||||||
let identifier = try container.decode(String.self, forKey: .assetIdentifier)
|
|
||||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else {
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .assetIdentifier, in: container, debugDescription: "Could not fetch asset with local identifier")
|
|
||||||
}
|
|
||||||
self = .asset(asset)
|
|
||||||
case "image":
|
|
||||||
let data = try container.decode(Data.self, forKey: .imageData)
|
|
||||||
if let type = try container.decodeIfPresent(UTType.self, forKey: .imageType) {
|
|
||||||
self = .image(data, originalType: type)
|
|
||||||
} else {
|
|
||||||
guard let image = UIImage(data: data) else {
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "CompositionAttachment data could not be decoded into UIImage")
|
|
||||||
}
|
|
||||||
let jpegData = image.jpegData(compressionQuality: 1)!
|
|
||||||
self = .image(jpegData, originalType: .jpeg)
|
|
||||||
}
|
|
||||||
case "drawing":
|
|
||||||
let drawingData = try container.decode(Data.self, forKey: .drawing)
|
|
||||||
let drawing = try PKDrawing(data: drawingData)
|
|
||||||
self = .drawing(drawing)
|
|
||||||
default:
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys: CodingKey {
|
|
||||||
case type
|
|
||||||
case imageData
|
|
||||||
case imageType
|
|
||||||
/// The local identifier of the PHAsset for this attachment
|
|
||||||
case assetIdentifier
|
|
||||||
/// The PKDrawing object for this attachment.
|
|
||||||
case drawing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AttachmentData: Equatable {
|
|
||||||
static func ==(lhs: AttachmentData, rhs: AttachmentData) -> Bool {
|
|
||||||
switch (lhs, rhs) {
|
|
||||||
case let (.asset(a), .asset(b)):
|
|
||||||
return a.localIdentifier == b.localIdentifier
|
|
||||||
case let (.image(a, originalType: aType), .image(b, originalType: bType)):
|
|
||||||
return a == b && aType == bType
|
|
||||||
case let (.video(a), .video(b)):
|
|
||||||
return a == b
|
|
||||||
case let (.drawing(a), .drawing(b)):
|
|
||||||
return a == b
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,13 @@ struct LanguagePicker: View {
|
||||||
if maybeIso639Code.last == "-" {
|
if maybeIso639Code.last == "-" {
|
||||||
maybeIso639Code = maybeIso639Code[..<maybeIso639Code.index(before: maybeIso639Code.endIndex)]
|
maybeIso639Code = maybeIso639Code[..<maybeIso639Code.index(before: maybeIso639Code.endIndex)]
|
||||||
}
|
}
|
||||||
let code = Locale.LanguageCode(String(maybeIso639Code))
|
let identifier = String(maybeIso639Code)
|
||||||
|
// mul (for multiple languages) and unk (unknown) are ISO codes, but not ones that akkoma permits, so we ignore them on all platforms
|
||||||
|
guard identifier != "mul",
|
||||||
|
identifier != "und" else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let code = Locale.LanguageCode(identifier)
|
||||||
if code.isISOLanguage {
|
if code.isISOLanguage {
|
||||||
return code
|
return code
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,13 +45,10 @@ struct LanguagePicker: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var codeFromPreferredLanguages: Locale.LanguageCode? {
|
private var codeFromPreferredLanguages: Locale.LanguageCode? {
|
||||||
if let identifier = Locale.preferredLanguages.first {
|
if let identifier = Locale.preferredLanguages.first,
|
||||||
let code = Locale.LanguageCode(identifier)
|
case let code = Locale.LanguageCode(identifier),
|
||||||
if code.isISOLanguage {
|
code.isISOLanguage {
|
||||||
return code
|
return code
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -66,6 +69,8 @@ struct LanguagePicker: View {
|
||||||
Text((languageCode.wrappedValue.identifier(.alpha2) ?? languageCode.wrappedValue.identifier).uppercased())
|
Text((languageCode.wrappedValue.identifier(.alpha2) ?? languageCode.wrappedValue.identifier).uppercased())
|
||||||
}
|
}
|
||||||
.accessibilityLabel("Post Language")
|
.accessibilityLabel("Post Language")
|
||||||
|
.padding(5)
|
||||||
|
.hoverEffect()
|
||||||
.sheet(isPresented: $isShowingSheet) {
|
.sheet(isPresented: $isShowingSheet) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
LanguagePickerList(languageCode: languageCode, hasChangedSelection: $hasChangedSelection, isPresented: $isShowingSheet)
|
LanguagePickerList(languageCode: languageCode, hasChangedSelection: $hasChangedSelection, isPresented: $isShowingSheet)
|
||||||
|
@ -138,10 +143,12 @@ private struct LanguagePickerList: View {
|
||||||
// make sure recents always contains the currently selected lang
|
// make sure recents always contains the currently selected lang
|
||||||
let recents = addRecentLang(languageCode)
|
let recents = addRecentLang(languageCode)
|
||||||
recentLangs = recents
|
recentLangs = recents
|
||||||
|
.filter { $0 != "mul" && $0 != "und" }
|
||||||
.map { Lang(code: .init($0)) }
|
.map { Lang(code: .init($0)) }
|
||||||
.sorted { $0.name < $1.name }
|
.sorted { $0.name < $1.name }
|
||||||
|
|
||||||
langs = Locale.LanguageCode.isoLanguageCodes
|
langs = Locale.LanguageCode.isoLanguageCodes
|
||||||
|
.filter { $0.identifier != "mul" && $0.identifier != "und" }
|
||||||
.map { Lang(code: $0) }
|
.map { Lang(code: $0) }
|
||||||
.sorted { $0.name < $1.name }
|
.sorted { $0.name < $1.name }
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,13 +162,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func closeWindow() {
|
|
||||||
guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func swizzleStatusBar() {
|
private func swizzleStatusBar() {
|
||||||
let selector = Selector(("handleTapAction:"))
|
let selector = Selector(("handleTapAction:"))
|
||||||
var originalIMP: IMP?
|
var originalIMP: IMP?
|
||||||
|
|
|
@ -41,22 +41,25 @@ struct MenuController {
|
||||||
static let prevSubTabCommand = UIKeyCommand(title: "Previous Sub Tab", action: #selector(TabbedPageViewController.selectPrevPage), input: "[", modifierFlags: [.command, .shift])
|
static let prevSubTabCommand = UIKeyCommand(title: "Previous Sub Tab", action: #selector(TabbedPageViewController.selectPrevPage), input: "[", modifierFlags: [.command, .shift])
|
||||||
|
|
||||||
static func buildMainMenu(builder: UIMenuBuilder) {
|
static func buildMainMenu(builder: UIMenuBuilder) {
|
||||||
builder.replace(menu: .file, with: buildFileMenu())
|
builder.replace(menu: .file, with: buildFileMenu(builder: builder))
|
||||||
builder.insertChild(buildSubTabMenu(), atStartOfMenu: .view)
|
builder.insertChild(buildSubTabMenu(), atStartOfMenu: .view)
|
||||||
builder.insertChild(buildSidebarShortcuts(), atStartOfMenu: .view)
|
builder.insertChild(buildSidebarShortcuts(), atStartOfMenu: .view)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func buildFileMenu() -> UIMenu {
|
private static func buildFileMenu(builder: UIMenuBuilder) -> UIMenu {
|
||||||
|
var children: [UIMenuElement] = [
|
||||||
|
composeCommand,
|
||||||
|
refreshCommand(discoverabilityTitle: nil),
|
||||||
|
]
|
||||||
|
if let close = builder.menu(for: .close) {
|
||||||
|
children.append(close)
|
||||||
|
}
|
||||||
return UIMenu(
|
return UIMenu(
|
||||||
title: "File",
|
title: "File",
|
||||||
image: nil,
|
image: nil,
|
||||||
identifier: nil,
|
identifier: nil,
|
||||||
options: [],
|
options: [],
|
||||||
children: [
|
children: children
|
||||||
composeCommand,
|
|
||||||
refreshCommand(discoverabilityTitle: nil),
|
|
||||||
UIKeyCommand(title: "Close", action: #selector(AppDelegate.closeWindow), input: "w", modifierFlags: .command),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
showAppOrOnboardingUI(session: session)
|
showAppOrOnboardingUI(session: session)
|
||||||
if connectionOptions.urlContexts.count > 0 {
|
if !connectionOptions.urlContexts.isEmpty {
|
||||||
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,14 +50,21 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||||
if URLContexts.count > 1 {
|
guard let url = URLContexts.first?.url,
|
||||||
fatalError("Cannot open more than 1 URL")
|
var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||||
|
let rootViewController else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = URLContexts.first!.url
|
if components.host == "compose" {
|
||||||
|
if let mastodonController = window!.windowScene!.session.mastodonController {
|
||||||
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
let draft = mastodonController.createDraft()
|
||||||
let rootViewController = rootViewController {
|
let text = components.queryItems?.first(where: { $0.name == "text" })?.value
|
||||||
|
draft.text = text ?? ""
|
||||||
|
rootViewController.compose(editing: draft, animated: true, isDucked: false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Assume anything else is a search query
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
let query = components.string!
|
let query = components.string!
|
||||||
rootViewController.performSearch(query: query)
|
rootViewController.performSearch(query: query)
|
||||||
|
|
|
@ -574,6 +574,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
var count = 0
|
var count = 0
|
||||||
while count < 5 {
|
while count < 5 {
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
let crumb = Breadcrumb(level: .info, category: "TimelineViewController")
|
||||||
|
crumb.message = "scrollToItem, attempt=\(count)"
|
||||||
|
SentrySDK.addBreadcrumb(crumb)
|
||||||
|
|
||||||
let origOffset = self.collectionView.contentOffset
|
let origOffset = self.collectionView.contentOffset
|
||||||
self.collectionView.layoutIfNeeded()
|
self.collectionView.layoutIfNeeded()
|
||||||
self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: false)
|
self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: false)
|
||||||
|
|
|
@ -211,9 +211,10 @@ extension TimelineLikeCollectionViewController {
|
||||||
extension TimelineLikeCollectionViewController {
|
extension TimelineLikeCollectionViewController {
|
||||||
// apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
|
// apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
|
||||||
// but we always want to update the data source on the main thread for consistency, so this method does that
|
// but we always want to update the data source on the main thread for consistency, so this method does that
|
||||||
@MainActor
|
|
||||||
func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
|
func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
|
||||||
await self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
|
await MainActor.run {
|
||||||
|
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
MARKETING_VERSION = 2023.8
|
MARKETING_VERSION = 2023.8
|
||||||
CURRENT_PROJECT_VERSION = 105
|
CURRENT_PROJECT_VERSION = 106
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||||
|
|
Loading…
Reference in New Issue