Compare commits

..

No commits in common. "1a767ff9103ae5b7fae2e3a5e78969b41819b786" and "f848bbf7c424d06a113f2a122e75662f75d5bfc3" have entirely different histories.

10 changed files with 50 additions and 108 deletions

View File

@ -56,7 +56,6 @@ private let imageType = UTType.image.identifier
private let mp4Type = UTType.mpeg4Movie.identifier private let mp4Type = UTType.mpeg4Movie.identifier
private let quickTimeType = UTType.quickTimeMovie.identifier private let quickTimeType = UTType.quickTimeMovie.identifier
private let dataType = UTType.data.identifier private let dataType = UTType.data.identifier
private let gifType = UTType.gif.identifier
extension CompositionAttachment: NSItemProviderWriting { extension CompositionAttachment: NSItemProviderWriting {
static var writableTypeIdentifiersForItemProvider: [String] { static var writableTypeIdentifiersForItemProvider: [String] {
@ -96,22 +95,20 @@ extension CompositionAttachment: NSItemProviderReading {
[typeIdentifier] + UIImage.readableTypeIdentifiersForItemProvider + [mp4Type, quickTimeType] + NSURL.readableTypeIdentifiersForItemProvider [typeIdentifier] + UIImage.readableTypeIdentifiersForItemProvider + [mp4Type, quickTimeType] + NSURL.readableTypeIdentifiersForItemProvider
} }
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> CompositionAttachment { static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
if typeIdentifier == CompositionAttachment.typeIdentifier { if typeIdentifier == CompositionAttachment.typeIdentifier {
return try PropertyListDecoder().decode(CompositionAttachment.self, from: data) return try PropertyListDecoder().decode(Self.self, from: data)
} else if typeIdentifier == gifType {
return CompositionAttachment(data: .gif(data))
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) { } else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
return CompositionAttachment(data: .image(image)) return CompositionAttachment(data: .image(image)) as! Self
} else if let type = UTType(typeIdentifier), type == .mpeg4Movie || type == .quickTimeMovie { } else if let type = UTType(typeIdentifier), type == .mpeg4Movie || type == .quickTimeMovie {
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let temporaryFileName = ProcessInfo().globallyUniqueString let temporaryFileName = ProcessInfo().globallyUniqueString
let fileExt = type.preferredFilenameExtension! let fileExt = type.preferredFilenameExtension!
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt)
try data.write(to: temporaryFileURL) try data.write(to: temporaryFileURL)
return CompositionAttachment(data: .video(temporaryFileURL)) return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL { } else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
return CompositionAttachment(data: .video(url)) return CompositionAttachment(data: .video(url)) as! Self
} else { } else {
throw ItemProviderError.incompatibleTypeIdentifier throw ItemProviderError.incompatibleTypeIdentifier
} }

View File

@ -16,7 +16,6 @@ enum CompositionAttachmentData {
case image(UIImage) case image(UIImage)
case video(URL) case video(URL)
case drawing(PKDrawing) case drawing(PKDrawing)
case gif(Data)
var type: AttachmentType { var type: AttachmentType {
switch self { switch self {
@ -28,8 +27,6 @@ enum CompositionAttachmentData {
return .video return .video
case .drawing(_): case .drawing(_):
return .image return .image
case .gif(_):
return .image
} }
} }
@ -122,8 +119,6 @@ enum CompositionAttachmentData {
case let .drawing(drawing): case let .drawing(drawing):
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1) let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
completion(.success((image.pngData()!, .png))) completion(.success((image.pngData()!, .png)))
case let .gif(data):
completion(.success((data, .gif)))
} }
} }
@ -196,8 +191,6 @@ extension CompositionAttachmentData: Codable {
try container.encode("drawing", forKey: .type) try container.encode("drawing", forKey: .type)
let drawingData = drawing.dataRepresentation() let drawingData = drawing.dataRepresentation()
try container.encode(drawingData, forKey: .drawing) try container.encode(drawingData, forKey: .drawing)
case .gif(_):
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "gif CompositionAttachments cannot be encoded"))
} }
} }
@ -221,7 +214,7 @@ extension CompositionAttachmentData: Codable {
let drawing = try PKDrawing(data: drawingData) let drawing = try PKDrawing(data: drawingData)
self = .drawing(drawing) self = .drawing(drawing)
default: default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing") throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'")
} }
} }

View File

@ -13,14 +13,18 @@ import AVKit
class AssetPreviewViewController: UIViewController { class AssetPreviewViewController: UIViewController {
let asset: PHAsset let attachment: CompositionAttachmentData
init(asset: PHAsset) { init(attachment: CompositionAttachmentData) {
self.asset = asset self.attachment = attachment
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
convenience init(asset: PHAsset) {
self.init(attachment: .asset(asset))
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -30,17 +34,27 @@ class AssetPreviewViewController: UIViewController {
view.backgroundColor = .black view.backgroundColor = .black
switch asset.mediaType { switch attachment {
case .image: case let .image(image):
if asset.mediaSubtypes.contains(.photoLive) { showImage(image)
showLivePhoto(asset) case let .video(url):
} else { showVideo(asset: AVURLAsset(url: url))
showAssetImage(asset) case let .asset(asset):
switch asset.mediaType {
case .image:
if asset.mediaSubtypes.contains(.photoLive) {
showLivePhoto(asset)
} else {
showAssetImage(asset)
}
case .video:
showAssetVideo(asset)
default:
fatalError("asset mediaType must be image or video")
} }
case .video: case let .drawing(drawing):
showAssetVideo(asset) let image = drawing.imageInLightMode(from: drawing.bounds)
default: showImage(image)
fatalError("asset mediaType must be image or video")
} }
} }

View File

@ -13,7 +13,6 @@ struct ComposeAttachmentImage: View {
let attachment: CompositionAttachment let attachment: CompositionAttachment
let fullSize: Bool let fullSize: Bool
@State private var gifData: Data? = nil
@State private var image: UIImage? = nil @State private var image: UIImage? = nil
@State private var imageContentMode: ContentMode = .fill @State private var imageContentMode: ContentMode = .fill
@State private var imageBackgroundColor: Color = .black @State private var imageBackgroundColor: Color = .black
@ -21,9 +20,7 @@ struct ComposeAttachmentImage: View {
@Environment(\.colorScheme) private var colorScheme: ColorScheme @Environment(\.colorScheme) private var colorScheme: ColorScheme
var body: some View { var body: some View {
if let gifData { if let image = image {
GIFViewWrapper(gifData: gifData)
} else if let image {
Image(uiImage: image) Image(uiImage: image)
.resizable() .resizable()
.aspectRatio(contentMode: imageContentMode) .aspectRatio(contentMode: imageContentMode)
@ -57,23 +54,9 @@ struct ComposeAttachmentImage: View {
// currently only used as thumbnail in ComposeAttachmentRow // currently only used as thumbnail in ComposeAttachmentRow
size = CGSize(width: 80, height: 80) size = CGSize(width: 80, height: 80)
} }
let isGIF = PHAssetResource.assetResources(for: asset).contains(where: { $0.uniformTypeIdentifier == UTType.gif.identifier }) PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
if isGIF { DispatchQueue.main.async {
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: nil) { data, typeIdentifier, orientation, info in self.image = image
if typeIdentifier == UTType.gif.identifier {
self.gifData = data
} else if let data {
let image = UIImage(data: data)
DispatchQueue.main.async {
self.image = image
}
}
}
} else {
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
DispatchQueue.main.async {
self.image = image
}
} }
} }
case let .video(url): case let .video(url):
@ -86,35 +69,10 @@ struct ComposeAttachmentImage: View {
image = drawing.imageInLightMode(from: drawing.bounds) image = drawing.imageInLightMode(from: drawing.bounds)
imageContentMode = .fit imageContentMode = .fit
imageBackgroundColor = .white imageBackgroundColor = .white
case let .gif(data):
self.gifData = data
} }
} }
} }
private struct GIFViewWrapper: UIViewRepresentable {
typealias UIViewType = GIFImageView
@State private var controller: GIFController
init(gifData: Data) {
self._controller = State(wrappedValue: GIFController(gifData: gifData))
}
func makeUIView(context: Context) -> GIFImageView {
let view = GIFImageView()
controller.attach(to: view)
controller.startAnimating()
view.contentMode = .scaleAspectFit
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
return view
}
func updateUIView(_ uiView: GIFImageView, context: Context) {
}
}
struct ComposeAttachmentImage_Previews: PreviewProvider { struct ComposeAttachmentImage_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ComposeAttachmentImage(attachment: CompositionAttachment(data: .image(UIImage())), fullSize: false) ComposeAttachmentImage(attachment: CompositionAttachment(data: .image(UIImage())), fullSize: false)

View File

@ -120,7 +120,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
} }
func textFieldDidEndEditing(_ textField: UITextField) { func textFieldDidEndEditing(_ textField: UITextField) {
uiState.currentInput = nil
updateAutocompleteState(textField: textField) updateAutocompleteState(textField: textField)
didEndEditing?() didEndEditing?()
} }

View File

@ -26,24 +26,22 @@ struct ComposeToolbar: View {
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) { HStack(spacing: 8) {
Button("CW") { Button("CW") {
draft.contentWarningEnabled.toggle() draft.contentWarningEnabled.toggle()
} }
.accessibilityLabel(draft.contentWarningEnabled ? "Remove content warning" : "Add content warning") .accessibilityLabel(draft.contentWarningEnabled ? "Remove content warning" : "Add content warning")
.padding(5)
.hoverEffect()
MenuPicker(selection: $draft.visibility, options: Self.visibilityOptions, buttonStyle: .iconOnly) MenuPicker(selection: $draft.visibility, options: Self.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)
if mastodonController.instanceFeatures.localOnlyPosts { if mastodonController.instanceFeatures.localOnlyPosts {
MenuPicker(selection: $draft.localOnly, options: [ MenuPicker(selection: $draft.localOnly, options: [
.init(value: true, title: "Local-only", subtitle: "Only \(mastodonController.accountInfo!.instanceURL.host!)", image: UIImage(named: "link.broken")), .init(value: true, title: "Local-only", subtitle: "Only \(mastodonController.accountInfo!.instanceURL.host!)", image: UIImage(named: "link.broken")),
.init(value: false, title: "Federated", image: UIImage(systemName: "link")) .init(value: false, title: "Federated", image: UIImage(systemName: "link"))
], buttonStyle: .iconOnly) ], buttonStyle: .iconOnly)
// .padding(.horizontal, -8) .padding(.horizontal, -8)
} }
if let currentInput = uiState.currentInput, currentInput.toolbarElements.contains(.emojiPicker) { if let currentInput = uiState.currentInput, currentInput.toolbarElements.contains(.emojiPicker) {
@ -52,8 +50,6 @@ struct ComposeToolbar: View {
} }
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
.font(.system(size: imageSize)) .font(.system(size: imageSize))
.padding(5)
.hoverEffect()
} }
if let currentInput = uiState.currentInput, if let currentInput = uiState.currentInput,
@ -72,8 +68,6 @@ struct ComposeToolbar: View {
} }
} }
.accessibilityLabel(format.accessibilityLabel) .accessibilityLabel(format.accessibilityLabel)
.padding(5)
.hoverEffect()
} }
} }
@ -82,8 +76,6 @@ struct ComposeToolbar: View {
Button(action: self.draftsButtonPressed) { Button(action: self.draftsButtonPressed) {
Text("Drafts") Text("Drafts")
} }
.padding(5)
.hoverEffect()
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.frame(minWidth: minWidth) .frame(minWidth: minWidth)

View File

@ -31,13 +31,6 @@ struct DraftsView: View {
} label: { } label: {
DraftView(draft: draft) DraftView(draft: draft)
} }
.contextMenu {
Button(role: .destructive) {
draftsManager.remove(draft)
} label: {
Label("Delete Draft", systemImage: "trash")
}
}
.onDrag { .onDrag {
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: mastodonController.accountInfo!.id) let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true activity.displaysAuxiliaryScene = true

View File

@ -106,6 +106,13 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.header, .pinned, .statuses])
snapshot.appendItems([.header(accountID)], toSection: .header)
dataSource.apply(snapshot, animatingDifferences: false)
state = .setupInitialSnapshot
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -174,18 +181,11 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
private func load() async { private func load() async {
guard isViewLoaded, guard isViewLoaded,
let accountID, let accountID,
state == .unloaded, state == .setupInitialSnapshot,
mastodonController.persistentContainer.account(for: accountID) != nil else { mastodonController.persistentContainer.account(for: accountID) != nil else {
return return
} }
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.header, .pinned, .statuses])
snapshot.appendItems([.header(accountID)], toSection: .header)
await apply(snapshot, animatingDifferences: false)
state = .setupInitialSnapshot
await controller.loadInitial() await controller.loadInitial()
await tryLoadPinned() await tryLoadPinned()

View File

@ -152,7 +152,6 @@ class ProfileViewController: UIViewController {
assert(!animated) assert(!animated)
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary // if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
new.initialHeaderMode = .createView new.initialHeaderMode = .createView
new.view.translatesAutoresizingMaskIntoConstraints = false
embedChild(new) embedChild(new)
self.currentIndex = index self.currentIndex = index
state = .idle state = .idle
@ -222,7 +221,6 @@ class ProfileViewController: UIViewController {
new.collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: additionalHeightNeededToMatchContentOffset, right: 0) new.collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: additionalHeightNeededToMatchContentOffset, right: 0)
} }
new.view.translatesAutoresizingMaskIntoConstraints = false
embedChild(new) embedChild(new)
new.view.transform = CGAffineTransform(translationX: direction * view.bounds.width, y: yTranslationToMatchOldContentOffset) new.view.transform = CGAffineTransform(translationX: direction * view.bounds.width, y: yTranslationToMatchOldContentOffset)
@ -258,7 +256,6 @@ class ProfileViewController: UIViewController {
animator.startAnimation() animator.startAnimation()
} else { } else {
old.removeViewAndController() old.removeViewAndController()
new.view.translatesAutoresizingMaskIntoConstraints = false
embedChild(new) embedChild(new)
completion?(true) completion?(true)
} }

View File

@ -44,7 +44,6 @@ struct MenuPicker<Value: Hashable>: UIViewRepresentable {
} }
}) })
button.accessibilityLabel = selectedOption.accessibilityLabel ?? selectedOption.title button.accessibilityLabel = selectedOption.accessibilityLabel ?? selectedOption.title
button.isPointerInteractionEnabled = buttonStyle == .iconOnly
} }
struct Option { struct Option {