Compare commits

..

No commits in common. "fe1db72f193bab9c5de0c7be1e96ed9bf2b9f177" and "4c82b1a341b12467f52d09574f0b27e0342824a6" have entirely different histories.

14 changed files with 109 additions and 308 deletions

View File

@ -227,7 +227,6 @@
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; }; D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; }; D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; }; D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; };
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; };
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; }; D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; }; D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; };
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -548,7 +547,6 @@
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; }; D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; }; D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; }; D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; };
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; }; D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; }; D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
@ -1253,7 +1251,6 @@
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */, D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */, D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */,
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */, D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
D67C57A721E2649B00C3118B /* Account Detail */, D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */, D67C57B021E28F9400C3118B /* Compose Status Reply */,
D626494023C122C800612E6E /* Asset Picker */, D626494023C122C800612E6E /* Asset Picker */,
@ -1797,7 +1794,6 @@
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */, D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */, D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */, D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,

View File

@ -124,19 +124,9 @@ extension StatusMO {
self.uri = status.uri self.uri = status.uri
self.url = status.url self.url = status.url
self.visibility = status.visibility self.visibility = status.visibility
if let existing = container.account(for: status.account.id, in: context) { self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context)
existing.updateFrom(apiAccount: status.account, container: container)
self.account = existing
} else {
self.account = AccountMO(apiAccount: status.account, container: container, context: context)
}
if let reblog = status.reblog { if let reblog = status.reblog {
if let existing = container.status(for: reblog.id, in: context) { self.reblog = container.status(for: reblog.id, in: context) ?? StatusMO(apiStatus: reblog, container: container, context: context)
existing.updateFrom(apiStatus: reblog, container: container)
self.reblog = existing
} else {
self.reblog = StatusMO(apiStatus: reblog, container: container, context: context)
}
} else { } else {
self.reblog = nil self.reblog = nil
} }

View File

@ -44,50 +44,45 @@ struct ComposeAttachmentsList: View {
} }
Button(action: self.addAttachment) { Button(action: self.addAttachment) {
if #available(iOS 14.0, *) {
Label("Add photo or video", systemImage: addButtonImageName)
} else {
HStack { HStack {
Image(systemName: addButtonImageName) addButtonImage
Text("Add photo or video") Text("Add image or video")
} }
} }
}
.disabled(!canAddAttachment)
.foregroundColor(.blue) .foregroundColor(.blue)
.frame(height: cellHeight / 2) .disabled(!canAddAttachment)
.frame(height: cellHeight)
.popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover) .popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .contextMenu {
Button(action: self.createDrawing) { Button(action: self.createDrawing) {
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
Label("Draw something", systemImage: "hand.draw") Label("Draw Something", systemImage: "hand.draw")
} else { } else {
HStack(alignment: .lastTextBaseline) { HStack {
Text("Draw Something")
Image(systemName: "hand.draw") Image(systemName: "hand.draw")
Text("Draw something")
} }
} }
} }
.disabled(!canAddAttachment) .disabled(!canAddAttachment)
.foregroundColor(.blue) }
.frame(height: cellHeight / 2)
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
} }
.frame(height: totalListHeight) .frame(height: totalListHeight)
.onAppear(perform: self.didAppear) .onAppear(perform: self.didAppear)
.onReceive(draft.$attachments, perform: self.attachmentsChanged) .onReceive(draft.$attachments, perform: self.attachmentsChanged)
} }
private var addButtonImageName: String { private var addButtonImage: Image {
let name: String
switch colorScheme { switch colorScheme {
case .dark: case .dark:
return "photo.fill" name = "photo.fill"
case .light: case .light:
return "photo" name = "photo"
@unknown default: @unknown default:
return "photo" name = "photo"
} }
return Image(systemName: name)
} }
private var canAddAttachment: Bool { private var canAddAttachment: Bool {
@ -95,14 +90,15 @@ struct ComposeAttachmentsList: View {
case .pleroma: case .pleroma:
return true return true
case .mastodon: case .mastodon:
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image } // todo: this technically allows invalid image/video combinations
return draft.attachments.count < 4
} }
} }
private var totalListHeight: CGFloat { private var totalListHeight: CGFloat {
let totalRowHeights = rowHeights.values.reduce(0, +) let totalRowHeights = rowHeights.values.reduce(0, +)
let totalPadding = CGFloat(draft.attachments.count) * cellPadding let totalPadding = CGFloat(draft.attachments.count) * cellPadding
let addButtonHeight = cellHeight + cellPadding * 2 let addButtonHeight = cellHeight + cellPadding
return totalRowHeights + totalPadding + addButtonHeight return totalRowHeights + totalPadding + addButtonHeight
} }

View File

@ -11,18 +11,24 @@ import Combine
struct ComposeContainerView: View { struct ComposeContainerView: View {
let mastodonController: MastodonController let mastodonController: MastodonController
let vcWidthSubject: PassthroughSubject<CGFloat, Never>
@ObservedObject var uiState: ComposeUIState @ObservedObject var uiState: ComposeUIState
init( init(
mastodonController: MastodonController, mastodonController: MastodonController,
vcWidthSubject: PassthroughSubject<CGFloat, Never>,
uiState: ComposeUIState uiState: ComposeUIState
) { ) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.vcWidthSubject = vcWidthSubject
self.uiState = uiState self.uiState = uiState
} }
var body: some View { var body: some View {
ComposeView(draft: uiState.draft) ComposeView(
draft: uiState.draft,
vcWidthSubject: vcWidthSubject
)
.environmentObject(mastodonController) .environmentObject(mastodonController)
.environmentObject(uiState) .environmentObject(uiState)
} }

View File

@ -20,7 +20,8 @@ struct ComposeCurrentAccount: View {
HStack(alignment: .top) { HStack(alignment: .top) {
ComposeAvatarImageView(url: account.avatar) ComposeAvatarImageView(url: account.avatar)
VStack(alignment: .leading) { VStack(alignment: .leading) {
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20) Text(verbatim: account.displayName)
.font(.system(size: 20, weight: .semibold))
.lineLimit(1) .lineLimit(1)
Text(verbatim: "@\(account.acct)") Text(verbatim: "@\(account.acct)")

View File

@ -16,14 +16,14 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
let mastodonController: MastodonController let mastodonController: MastodonController
let uiState: ComposeUIState let uiState: ComposeUIState
// storing the width in the UI state and having SwiftUI listen to it via @ObservedObject doesn't work
// it ends up spinning forever
let widthSubject = PassthroughSubject<CGFloat, Never>()
var draft: Draft { uiState.draft } var draft: Draft { uiState.draft }
private var cancellables = [AnyCancellable]() private var cancellables = [AnyCancellable]()
private var keyboardHeight: CGFloat = 0
private var toolbarHeight: CGFloat = 44
private var mainToolbar: UIToolbar! private var mainToolbar: UIToolbar!
private var inputAccessoryToolbar: UIToolbar! private var inputAccessoryToolbar: UIToolbar!
private var visibilityBarButtonItems = [UIBarButtonItem]() private var visibilityBarButtonItems = [UIBarButtonItem]()
@ -42,6 +42,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
// to use as the UIHostingController type parameter // to use as the UIHostingController type parameter
let container = ComposeContainerView( let container = ComposeContainerView(
mastodonController: mastodonController, mastodonController: mastodonController,
vcWidthSubject: widthSubject,
uiState: uiState uiState: uiState
) )
super.init(rootView: container) super.init(rootView: container)
@ -57,7 +58,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
// add the height of the toolbar itself to the bottom of the safe area so content inside SwiftUI ScrollView doesn't underflow it // add the height of the toolbar itself to the bottom of the safe area so content inside SwiftUI ScrollView doesn't underflow it
updateAdditionalSafeAreaInsets() additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 44, right: 0)
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self) pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
@ -81,6 +82,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
widthSubject.send(view.bounds.width)
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
@ -109,15 +116,9 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
let toolbar = UIToolbar() let toolbar = UIToolbar()
toolbar.translatesAutoresizingMaskIntoConstraints = false toolbar.translatesAutoresizingMaskIntoConstraints = false
let visibilityAction: Selector? let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: #selector(visibilityButtonPressed))
if #available(iOS 14.0, *) { visibilityItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
visibilityAction = nil
} else {
visibilityAction = #selector(visibilityButtonPressed(_:))
}
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: visibilityAction)
visibilityBarButtonItems.append(visibilityItem) visibilityBarButtonItems.append(visibilityItem)
visibilityChanged(draft.visibility)
toolbar.items = [ toolbar.items = [
UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)), UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)),
@ -128,9 +129,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
return toolbar return toolbar
} }
private func updateAdditionalSafeAreaInsets() {
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight + keyboardHeight, right: 0)
}
@objc private func keyboardWillShow(_ notification: Foundation.Notification) { @objc private func keyboardWillShow(_ notification: Foundation.Notification) {
keyboardWillShow(accessoryView: inputAccessoryToolbar, notification: notification) keyboardWillShow(accessoryView: inputAccessoryToolbar, notification: notification)
@ -141,17 +139,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
accessoryView.alpha = 1 accessoryView.alpha = 1
accessoryView.isHidden = false accessoryView.isHidden = false
// on iOS 14, SwiftUI safe area automatically includes the keyboard
if #available(iOS 14.0, *) {
} else {
let userInfo = notification.userInfo!
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
// temporarily reset add'l safe area insets so we can access the default inset
additionalSafeAreaInsets = .zero
keyboardHeight = frame.height - view.safeAreaInsets.bottom - accessoryView.frame.height
updateAdditionalSafeAreaInsets()
}
} }
@objc private func keyboardWillHide(_ notification: Foundation.Notification) { @objc private func keyboardWillHide(_ notification: Foundation.Notification) {
@ -184,13 +171,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
} completion: { (finished) in } completion: { (finished) in
accessoryView.alpha = 1 accessoryView.alpha = 1
} }
// on iOS 14, SwiftUI safe area automatically includes the keyboard
if #available(iOS 14.0, *) {
} else {
keyboardHeight = 0
updateAdditionalSafeAreaInsets()
}
} }
@objc private func keyboardDidHide(_ notification: Foundation.Notification) { @objc private func keyboardDidHide(_ notification: Foundation.Notification) {
@ -205,15 +185,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
for item in visibilityBarButtonItems { for item in visibilityBarButtonItems {
item.image = UIImage(systemName: newVisibility.imageName) item.image = UIImage(systemName: newVisibility.imageName)
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName) item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
if #available(iOS 14.0, *) {
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
self.draft.visibility = visibility
}
}
item.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
}
} }
} }
@ -223,8 +194,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
case .pleroma: case .pleroma:
return true return true
case .mastodon: case .mastodon:
guard draft.attachments.allSatisfy({ $0.data.type == .image }) else { return false } // todo: this technically allows invalid video/image combinations
// todo: if providers are videos, this technically allows invalid video/image combinations
return itemProviders.count + draft.attachments.count <= 4 return itemProviders.count + draft.attachments.count <= 4
} }
} }
@ -247,15 +217,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
} }
@objc func visibilityButtonPressed(_ sender: UIBarButtonItem) { @objc func visibilityButtonPressed(_ sender: UIBarButtonItem) {
// if #available(iOS 14.0, *) {
// } else {
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
guard let visibility = visibility else { return } guard let visibility = visibility else { return }
self.draft.visibility = visibility self.draft.visibility = visibility
} }
alertController.popoverPresentationController?.barButtonItem = sender alertController.popoverPresentationController?.barButtonItem = sender
present(alertController, animated: true) present(alertController, animated: true)
// }
} }
@objc func draftsButtonPresed() { @objc func draftsButtonPresed() {
@ -266,6 +233,17 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
} }
extension ComposeHostingController {
struct EnvironmentWrappingView<Content: View, EnvironmentObject: ObservableObject>: View {
let content: Content
let environmentObject: EnvironmentObject
var body: some View {
content.environmentObject(environmentObject)
}
}
}
extension ComposeHostingController: ComposeUIStateDelegate { extension ComposeHostingController: ComposeUIStateDelegate {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self } var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }

View File

@ -12,33 +12,37 @@ struct ComposeReplyContentView: UIViewRepresentable {
typealias UIViewType = ComposeReplyContentTextView typealias UIViewType = ComposeReplyContentTextView
let status: StatusMO let status: StatusMO
let maxWidth: CGFloat
@EnvironmentObject var mastodonController: MastodonController @EnvironmentObject var mastodonController: MastodonController
let heightChanged: (CGFloat) -> Void
func makeUIView(context: Context) -> ComposeReplyContentTextView { func makeUIView(context: Context) -> ComposeReplyContentTextView {
let view = ComposeReplyContentTextView() let view = ComposeReplyContentTextView()
view.overrideMastodonController = mastodonController view.overrideMastodonController = mastodonController
view.setTextFrom(status: status) view.setTextFrom(status: status)
view.isScrollEnabled = false
view.isUserInteractionEnabled = false view.isUserInteractionEnabled = false
view.backgroundColor = .clear view.backgroundColor = .clear
view.maxWidth = maxWidth
return view return view
} }
func updateUIView(_ uiView: ComposeReplyContentTextView, context: Context) { func updateUIView(_ uiView: ComposeReplyContentTextView, context: Context) {
uiView.heightChanged = heightChanged uiView.constraint.constant = maxWidth
} }
} }
class ComposeReplyContentTextView: StatusContentTextView { class ComposeReplyContentTextView: StatusContentTextView {
var heightChanged: ((CGFloat) -> Void)?
override func layoutSubviews() { var maxWidth: CGFloat!
super.layoutSubviews() var constraint: NSLayoutConstraint!
heightChanged?(contentSize.height) override func didMoveToSuperview() {
super.didMoveToSuperview()
translatesAutoresizingMaskIntoConstraints = false
constraint = widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth)
constraint.isActive = true
} }
} }

View File

@ -10,10 +10,8 @@ import SwiftUI
struct ComposeReplyView: View { struct ComposeReplyView: View {
let status: StatusMO let status: StatusMO
let maxWidth: CGFloat
let stackPadding: CGFloat let stackPadding: CGFloat
let outerMinY: CGFloat
@State private var contentHeight: CGFloat?
private let horizSpacing: CGFloat = 8 private let horizSpacing: CGFloat = 8
@ -24,7 +22,8 @@ struct ComposeReplyView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
HStack { HStack {
AccountDisplayNameLabel(account: status.account, fontSize: 17) Text(verbatim: status.account.displayName)
.font(.system(size: 17, weight: .semibold))
.lineLimit(1) .lineLimit(1)
.layoutPriority(1) .layoutPriority(1)
@ -36,10 +35,7 @@ struct ComposeReplyView: View {
Spacer() Spacer()
} }
ComposeReplyContentView(status: status) { (newHeight) in ComposeReplyContentView(status: status, maxWidth: maxWidth - 50 - horizSpacing + 4)
self.contentHeight = newHeight
}
.frame(height: contentHeight)
.offset(x: -4, y: -8) .offset(x: -4, y: -8)
.padding(.bottom, -8) .padding(.bottom, -8)
} }
@ -49,11 +45,8 @@ struct ComposeReplyView: View {
} }
private func replyAvatarImage(geometry: GeometryProxy) -> some View { private func replyAvatarImage(geometry: GeometryProxy) -> some View {
// using named coordinate spaces produces an incorrect scroll offset on iOS 13, let scrollOffset = geometry.frame(in: .named("outer")).minY - stackPadding
// so simply compare the geometry inside and outside the scroll view in the global coordinate space let offset = min(max(-scrollOffset, 0), geometry.size.height - 50 - 8)
var scrollOffset = outerMinY - geometry.frame(in: .global).minY
scrollOffset += stackPadding
let offset = min(max(scrollOffset, 0), geometry.size.height - 50 - stackPadding)
return ComposeAvatarImageView(url: status.account.avatar) return ComposeAvatarImageView(url: status.account.avatar)
.offset(x: 0, y: offset) .offset(x: 0, y: offset)
} }

View File

@ -12,8 +12,10 @@ import Combine
struct ComposeView: View { struct ComposeView: View {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
let vcWidthSubject: PassthroughSubject<CGFloat, Never>
@EnvironmentObject var mastodonController: MastodonController @EnvironmentObject var mastodonController: MastodonController
@State var viewControllerWidth: CGFloat = 0
@EnvironmentObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
@State var isPosting = false @State var isPosting = false
@State var postProgress: Double = 0 @State var postProgress: Double = 0
@ -23,8 +25,12 @@ struct ComposeView: View {
private let stackPadding: CGFloat = 8 private let stackPadding: CGFloat = 8
init(draft: Draft) { init(
draft: Draft,
vcWidthSubject: PassthroughSubject<CGFloat, Never>
) {
self.draft = draft self.draft = draft
self.vcWidthSubject = vcWidthSubject
} }
var charactersRemaining: Int { var charactersRemaining: Int {
@ -57,17 +63,17 @@ struct ComposeView: View {
var mostOfTheBody: some View { var mostOfTheBody: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
GeometryReader { (outer) in
ScrollView(.vertical) { ScrollView(.vertical) {
mainStack(outerMinY: outer.frame(in: .global).minY) mainStack
}
} }
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149 // can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
WrappedProgressView(value: postProgress, total: postTotalProgress) WrappedProgressView(value: postProgress, total: postTotalProgress)
} }
.coordinateSpace(name: "outer")
.onAppear(perform: self.didAppear) .onAppear(perform: self.didAppear)
.navigationBarTitle("Compose") .navigationBarTitle("Compose")
.onReceive(vcWidthSubject) { self.viewControllerWidth = $0 }
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet) .actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
.alert(isPresented: $isShowingPostErrorAlert) { .alert(isPresented: $isShowingPostErrorAlert) {
Alert( Alert(
@ -78,14 +84,14 @@ struct ComposeView: View {
} }
} }
func mainStack(outerMinY: CGFloat) -> some View { var mainStack: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
if let id = draft.inReplyToID, if let id = draft.inReplyToID,
let status = mastodonController.persistentContainer.status(for: id) { let status = mastodonController.persistentContainer.status(for: id) {
ComposeReplyView( ComposeReplyView(
status: status, status: status,
stackPadding: stackPadding, maxWidth: viewControllerWidth - (2 * stackPadding),
outerMinY: outerMinY stackPadding: stackPadding
) )
} }
@ -145,13 +151,7 @@ struct ComposeView: View {
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear // draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
uiState.delegate?.dismissCompose() uiState.delegate?.dismissCompose()
} else { } else {
// if the draft doesn't have content, it doesn't need to be saved
if draft.hasContent {
uiState.isShowingSaveDraftSheet = true uiState.isShowingSaveDraftSheet = true
} else {
DraftsManager.shared.remove(draft)
uiState.delegate?.dismissCompose()
}
} }
} }

View File

@ -64,14 +64,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
textView.textContainer.lineBreakMode = .byWordWrapping textView.textContainer.lineBreakMode = .byWordWrapping
context.coordinator.textView = textView context.coordinator.textView = textView
let visibilityAction: Selector? let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: #selector(ComposeHostingController.visibilityButtonPressed(_:)))
if #available(iOS 14.0, *) {
visibilityAction = nil
} else {
visibilityAction = #selector(ComposeHostingController.visibilityButtonPressed(_:))
}
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: visibilityAction)
updateVisibilityMenu(visibilityButton)
let toolbar = UIToolbar() let toolbar = UIToolbar()
toolbar.translatesAutoresizingMaskIntoConstraints = false toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbar.items = [ toolbar.items = [
@ -125,33 +118,16 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
return formatButtons return formatButtons
} }
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
if #available(iOS 14.0, *) {
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
let state = visibility == self.visibility ? UIMenuElement.State.on : .off
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
self.uiState.draft.visibility = visibility
}
}
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
}
}
func updateUIView(_ uiView: UITextView, context: Context) { func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text uiView.text = text
if let visibilityButton = visibilityButton { visibilityButton?.image = UIImage(systemName: visibility.imageName)
visibilityButton.image = UIImage(systemName: visibility.imageName)
updateVisibilityMenu(visibilityButton)
}
context.coordinator.text = $text context.coordinator.text = $text
context.coordinator.didChange = textDidChange context.coordinator.didChange = textDidChange
context.coordinator.uiState = uiState context.coordinator.uiState = uiState
if becomeFirstResponder { if becomeFirstResponder {
DispatchQueue.main.async {
// calling becomeFirstResponder during the SwiftUI update causes a crash on iOS 13
uiView.becomeFirstResponder() uiView.becomeFirstResponder()
// can't update @State vars during the SwiftUI update DispatchQueue.main.async {
becomeFirstResponder = false becomeFirstResponder = false
} }
} }

View File

@ -58,16 +58,6 @@ class ProfileViewController: UIPageViewController {
view.backgroundColor = .systemBackground view.backgroundColor = .systemBackground
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
if #available(iOS 14.0, *) {
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
self.composeDirectMentioning()
})
])
}
navigationItem.rightBarButtonItem = composeButton
headerView = ProfileHeaderView.create() headerView = ProfileHeaderView.create()
headerView.delegate = self headerView.delegate = self
@ -172,22 +162,6 @@ class ProfileViewController: UIPageViewController {
} }
} }
// MARK: Interaction
@objc private func composeMentioning() {
if let account = mastodonController.persistentContainer.account(for: accountID) {
compose(mentioningAcct: account.acct)
}
}
private func composeDirectMentioning() {
if let account = mastodonController.persistentContainer.account(for: accountID) {
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
draft.visibility = .direct
compose(editing: draft)
}
}
} }
extension ProfileViewController: TuskerNavigationDelegate { extension ProfileViewController: TuskerNavigationDelegate {

View File

@ -79,7 +79,8 @@ extension TuskerNavigationDelegate {
show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self) show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
} }
func compose(editing draft: Draft) { func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
let compose = ComposeHostingController(draft: draft, mastodonController: apiController) let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
let vc = UINavigationController(rootViewController: compose) let vc = UINavigationController(rootViewController: compose)
@ -87,11 +88,6 @@ extension TuskerNavigationDelegate {
present(vc, animated: true) present(vc, animated: true)
} }
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
compose(editing: draft)
}
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
vc.animationSourceView = sourceView vc.animationSourceView = sourceView

View File

@ -1,109 +0,0 @@
//
// AccountDisplayNameLabel.swift
// Tusker
//
// Created by Shadowfacts on 9/7/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import SwiftUI
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
struct AccountDisplayNameLabel: View {
let account: AccountMO
let fontSize: Int
@State var text: Text
@State var emojiRequests = [ImageCache.Request]()
init(account: AccountMO, fontSize: Int) {
self.account = account
self.fontSize = fontSize
self._text = State(initialValue: Text(verbatim: account.displayName))
}
var body: some View {
if #available(iOS 14.0, *) {
text
.font(.system(size: CGFloat(fontSize), weight: .semibold))
.onAppear(perform: self.loadEmojis)
} else {
text
.font(.system(size: CGFloat(fontSize), weight: .semibold))
}
}
// embedding Image inside Text is only available on iOS 14
@available(iOS 14.0, *)
private func loadEmojis() {
let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName)
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
guard !matches.isEmpty else { return }
let emojiImages = CachedDictionary<Image>(name: "AcccountDisplayNameLabel Emoji Images")
let group = DispatchGroup()
for emoji in account.emojis {
guard matches.contains(where: { (match) in
let matchShortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
return emoji.shortcode == matchShortcode
}) else {
continue
}
group.enter()
let request = ImageCache.emojis.get(emoji.url) { (data) in
defer { group.leave() }
guard let data = data, let image = UIImage(data: data) else { return }
let size = CGSize(width: fontSize, height: fontSize)
let renderer = UIGraphicsImageRenderer(size: size)
let resized = renderer.image { (ctx) in
image.draw(in: CGRect(origin: .zero, size: size))
}
emojiImages[emoji.shortcode] = Image(uiImage: resized)
}
if let request = request {
emojiRequests.append(request)
}
}
group.notify(queue: .main) {
var text: Text?
var endIndex = account.displayName.utf16.count
// iterate backwards as to not alter the indices of earlier matches
for match in matches.reversed() {
let shortcode = (account.displayName as NSString).substring(with: match.range(at: 1))
guard let image = emojiImages[shortcode] else { continue }
let afterCurrentMatch = (account.displayName as NSString).substring(with: NSRange(location: match.range.upperBound, length: endIndex - match.range.upperBound))
if let subsequent = text {
text = Text(image) + Text(verbatim: afterCurrentMatch) + subsequent
} else {
text = Text(image) + Text(verbatim: afterCurrentMatch)
}
endIndex = match.range.lowerBound
}
let beforeLastMatch = (account.displayName as NSString).substring(to: endIndex)
if let text = text {
self.text = Text(verbatim: beforeLastMatch) + text
} else {
self.text = Text(verbatim: beforeLastMatch)
}
}
}
}
//struct AccountDisplayNameLabel_Previews: PreviewProvider {
// static var previews: some View {
// AccountDisplayNameLabel()
// }
//}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17147" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17120"/>
<capability name="Image references" minToolsVersion="12.0"/> <capability name="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -113,7 +113,7 @@
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HZv-qj-gi6"> <stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HZv-qj-gi6">
<rect key="frame" x="0.0" y="188.5" width="142" height="18"/> <rect key="frame" x="0.0" y="188.5" width="142" height="18"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bs-Vjq"> <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bs-Vjq">
<rect key="frame" x="0.0" y="0.0" width="75" height="18"/> <rect key="frame" x="0.0" y="0.0" width="75" height="18"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="18" id="F9W-LW-swd"/> <constraint firstAttribute="height" constant="18" id="F9W-LW-swd"/>
@ -125,7 +125,7 @@
<action selector="totalFavoritesPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="D3Y-YB-bqP"/> <action selector="totalFavoritesPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="D3Y-YB-bqP"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB">
<rect key="frame" x="83" y="0.0" width="59" height="18"/> <rect key="frame" x="83" y="0.0" width="59" height="18"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="18" id="k0P-W7-wMF"/> <constraint firstAttribute="height" constant="18" id="k0P-W7-wMF"/>
@ -247,7 +247,7 @@
</view> </view>
</objects> </objects>
<resources> <resources>
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/> <image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="104"/>
<image name="chevron.down" catalog="system" width="128" height="72"/> <image name="chevron.down" catalog="system" width="128" height="72"/>
<image name="ellipsis" catalog="system" width="128" height="37"/> <image name="ellipsis" catalog="system" width="128" height="37"/>
<image name="globe" catalog="system" width="128" height="121"/> <image name="globe" catalog="system" width="128" height="121"/>