Compare commits
13 Commits
4c82b1a341
...
fe1db72f19
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fe1db72f19 | |
Shadowfacts | b4ddb8f533 | |
Shadowfacts | 9a4ddfea3f | |
Shadowfacts | dd8a196630 | |
Shadowfacts | 3da7aacb35 | |
Shadowfacts | 39c8162931 | |
Shadowfacts | fe95cb9e1a | |
Shadowfacts | ec2d510be2 | |
Shadowfacts | 262aadf807 | |
Shadowfacts | 9dce94c014 | |
Shadowfacts | d008b882cb | |
Shadowfacts | 3d13df87f0 | |
Shadowfacts | f0582739cc |
|
@ -227,6 +227,7 @@
|
|||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
|
||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
|
||||
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 */; };
|
||||
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, ); }; };
|
||||
|
@ -547,6 +548,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1251,6 +1253,7 @@
|
|||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */,
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||
D626494023C122C800612E6E /* Asset Picker */,
|
||||
|
@ -1794,6 +1797,7 @@
|
|||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||
|
|
|
@ -124,9 +124,19 @@ extension StatusMO {
|
|||
self.uri = status.uri
|
||||
self.url = status.url
|
||||
self.visibility = status.visibility
|
||||
self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context)
|
||||
if let existing = container.account(for: status.account.id, in: 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 {
|
||||
self.reblog = container.status(for: reblog.id, in: context) ?? StatusMO(apiStatus: reblog, container: container, context: context)
|
||||
if let existing = container.status(for: reblog.id, in: context) {
|
||||
existing.updateFrom(apiStatus: reblog, container: container)
|
||||
self.reblog = existing
|
||||
} else {
|
||||
self.reblog = StatusMO(apiStatus: reblog, container: container, context: context)
|
||||
}
|
||||
} else {
|
||||
self.reblog = nil
|
||||
}
|
||||
|
|
|
@ -44,45 +44,50 @@ struct ComposeAttachmentsList: View {
|
|||
}
|
||||
|
||||
Button(action: self.addAttachment) {
|
||||
HStack {
|
||||
addButtonImage
|
||||
Text("Add image or video")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
.disabled(!canAddAttachment)
|
||||
.frame(height: cellHeight)
|
||||
.popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
|
||||
.contextMenu {
|
||||
Button(action: self.createDrawing) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Draw Something", systemImage: "hand.draw")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Draw Something")
|
||||
Image(systemName: "hand.draw")
|
||||
}
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Add photo or video", systemImage: addButtonImageName)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: addButtonImageName)
|
||||
Text("Add photo or video")
|
||||
}
|
||||
}
|
||||
.disabled(!canAddAttachment)
|
||||
}
|
||||
.disabled(!canAddAttachment)
|
||||
.foregroundColor(.blue)
|
||||
.frame(height: cellHeight / 2)
|
||||
.popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
|
||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||
|
||||
Button(action: self.createDrawing) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Draw something", systemImage: "hand.draw")
|
||||
} else {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Image(systemName: "hand.draw")
|
||||
Text("Draw something")
|
||||
}
|
||||
}
|
||||
}
|
||||
.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)
|
||||
.onAppear(perform: self.didAppear)
|
||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
||||
}
|
||||
|
||||
private var addButtonImage: Image {
|
||||
let name: String
|
||||
private var addButtonImageName: String {
|
||||
switch colorScheme {
|
||||
case .dark:
|
||||
name = "photo.fill"
|
||||
return "photo.fill"
|
||||
case .light:
|
||||
name = "photo"
|
||||
return "photo"
|
||||
@unknown default:
|
||||
name = "photo"
|
||||
return "photo"
|
||||
}
|
||||
return Image(systemName: name)
|
||||
}
|
||||
|
||||
private var canAddAttachment: Bool {
|
||||
|
@ -90,15 +95,14 @@ struct ComposeAttachmentsList: View {
|
|||
case .pleroma:
|
||||
return true
|
||||
case .mastodon:
|
||||
// todo: this technically allows invalid image/video combinations
|
||||
return draft.attachments.count < 4
|
||||
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image }
|
||||
}
|
||||
}
|
||||
|
||||
private var totalListHeight: CGFloat {
|
||||
let totalRowHeights = rowHeights.values.reduce(0, +)
|
||||
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
|
||||
let addButtonHeight = cellHeight + cellPadding
|
||||
let addButtonHeight = cellHeight + cellPadding * 2
|
||||
return totalRowHeights + totalPadding + addButtonHeight
|
||||
}
|
||||
|
||||
|
|
|
@ -11,24 +11,18 @@ import Combine
|
|||
|
||||
struct ComposeContainerView: View {
|
||||
let mastodonController: MastodonController
|
||||
let vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
||||
@ObservedObject var uiState: ComposeUIState
|
||||
|
||||
init(
|
||||
mastodonController: MastodonController,
|
||||
vcWidthSubject: PassthroughSubject<CGFloat, Never>,
|
||||
uiState: ComposeUIState
|
||||
) {
|
||||
self.mastodonController = mastodonController
|
||||
self.vcWidthSubject = vcWidthSubject
|
||||
self.uiState = uiState
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ComposeView(
|
||||
draft: uiState.draft,
|
||||
vcWidthSubject: vcWidthSubject
|
||||
)
|
||||
ComposeView(draft: uiState.draft)
|
||||
.environmentObject(mastodonController)
|
||||
.environmentObject(uiState)
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@ struct ComposeCurrentAccount: View {
|
|||
HStack(alignment: .top) {
|
||||
ComposeAvatarImageView(url: account.avatar)
|
||||
VStack(alignment: .leading) {
|
||||
Text(verbatim: account.displayName)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(verbatim: "@\(account.acct)")
|
||||
|
|
|
@ -16,14 +16,14 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
let mastodonController: MastodonController
|
||||
|
||||
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 }
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
private var keyboardHeight: CGFloat = 0
|
||||
private var toolbarHeight: CGFloat = 44
|
||||
|
||||
private var mainToolbar: UIToolbar!
|
||||
private var inputAccessoryToolbar: UIToolbar!
|
||||
private var visibilityBarButtonItems = [UIBarButtonItem]()
|
||||
|
@ -42,7 +42,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
// to use as the UIHostingController type parameter
|
||||
let container = ComposeContainerView(
|
||||
mastodonController: mastodonController,
|
||||
vcWidthSubject: widthSubject,
|
||||
uiState: uiState
|
||||
)
|
||||
super.init(rootView: container)
|
||||
|
@ -58,7 +57,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
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
|
||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 44, right: 0)
|
||||
updateAdditionalSafeAreaInsets()
|
||||
|
||||
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||
|
||||
|
@ -82,12 +81,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
widthSubject.send(view.bounds.width)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
|
@ -116,9 +109,15 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
let toolbar = UIToolbar()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: #selector(visibilityButtonPressed))
|
||||
visibilityItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||
let visibilityAction: Selector?
|
||||
if #available(iOS 14.0, *) {
|
||||
visibilityAction = nil
|
||||
} else {
|
||||
visibilityAction = #selector(visibilityButtonPressed(_:))
|
||||
}
|
||||
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: visibilityAction)
|
||||
visibilityBarButtonItems.append(visibilityItem)
|
||||
visibilityChanged(draft.visibility)
|
||||
|
||||
toolbar.items = [
|
||||
UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)),
|
||||
|
@ -129,6 +128,9 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
return toolbar
|
||||
}
|
||||
|
||||
private func updateAdditionalSafeAreaInsets() {
|
||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight + keyboardHeight, right: 0)
|
||||
}
|
||||
|
||||
@objc private func keyboardWillShow(_ notification: Foundation.Notification) {
|
||||
keyboardWillShow(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||
|
@ -139,6 +141,17 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
|
||||
accessoryView.alpha = 1
|
||||
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) {
|
||||
|
@ -171,6 +184,13 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
} completion: { (finished) in
|
||||
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) {
|
||||
|
@ -185,6 +205,15 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
for item in visibilityBarButtonItems {
|
||||
item.image = UIImage(systemName: newVisibility.imageName)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +223,8 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
case .pleroma:
|
||||
return true
|
||||
case .mastodon:
|
||||
// todo: this technically allows invalid video/image combinations
|
||||
guard draft.attachments.allSatisfy({ $0.data.type == .image }) else { return false }
|
||||
// todo: if providers are videos, this technically allows invalid video/image combinations
|
||||
return itemProviders.count + draft.attachments.count <= 4
|
||||
}
|
||||
}
|
||||
|
@ -217,12 +247,15 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
}
|
||||
|
||||
@objc func visibilityButtonPressed(_ sender: UIBarButtonItem) {
|
||||
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
|
||||
guard let visibility = visibility else { return }
|
||||
self.draft.visibility = visibility
|
||||
}
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
present(alertController, animated: true)
|
||||
// if #available(iOS 14.0, *) {
|
||||
// } else {
|
||||
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
|
||||
guard let visibility = visibility else { return }
|
||||
self.draft.visibility = visibility
|
||||
}
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
present(alertController, animated: true)
|
||||
// }
|
||||
}
|
||||
|
||||
@objc func draftsButtonPresed() {
|
||||
|
@ -233,17 +266,6 @@ 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 {
|
||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }
|
||||
|
||||
|
|
|
@ -12,37 +12,33 @@ struct ComposeReplyContentView: UIViewRepresentable {
|
|||
typealias UIViewType = ComposeReplyContentTextView
|
||||
|
||||
let status: StatusMO
|
||||
let maxWidth: CGFloat
|
||||
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
|
||||
let heightChanged: (CGFloat) -> Void
|
||||
|
||||
func makeUIView(context: Context) -> ComposeReplyContentTextView {
|
||||
let view = ComposeReplyContentTextView()
|
||||
view.overrideMastodonController = mastodonController
|
||||
view.setTextFrom(status: status)
|
||||
view.isScrollEnabled = false
|
||||
view.isUserInteractionEnabled = false
|
||||
view.backgroundColor = .clear
|
||||
view.maxWidth = maxWidth
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: ComposeReplyContentTextView, context: Context) {
|
||||
uiView.constraint.constant = maxWidth
|
||||
uiView.heightChanged = heightChanged
|
||||
}
|
||||
}
|
||||
|
||||
class ComposeReplyContentTextView: StatusContentTextView {
|
||||
var heightChanged: ((CGFloat) -> Void)?
|
||||
|
||||
var maxWidth: CGFloat!
|
||||
var constraint: NSLayoutConstraint!
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
constraint = widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth)
|
||||
constraint.isActive = true
|
||||
heightChanged?(contentSize.height)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,8 +10,10 @@ import SwiftUI
|
|||
|
||||
struct ComposeReplyView: View {
|
||||
let status: StatusMO
|
||||
let maxWidth: CGFloat
|
||||
let stackPadding: CGFloat
|
||||
let outerMinY: CGFloat
|
||||
|
||||
@State private var contentHeight: CGFloat?
|
||||
|
||||
private let horizSpacing: CGFloat = 8
|
||||
|
||||
|
@ -22,8 +24,7 @@ struct ComposeReplyView: View {
|
|||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
Text(verbatim: status.account.displayName)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
AccountDisplayNameLabel(account: status.account, fontSize: 17)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
|
||||
|
@ -35,7 +36,10 @@ struct ComposeReplyView: View {
|
|||
Spacer()
|
||||
}
|
||||
|
||||
ComposeReplyContentView(status: status, maxWidth: maxWidth - 50 - horizSpacing + 4)
|
||||
ComposeReplyContentView(status: status) { (newHeight) in
|
||||
self.contentHeight = newHeight
|
||||
}
|
||||
.frame(height: contentHeight)
|
||||
.offset(x: -4, y: -8)
|
||||
.padding(.bottom, -8)
|
||||
}
|
||||
|
@ -45,8 +49,11 @@ struct ComposeReplyView: View {
|
|||
}
|
||||
|
||||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||
let scrollOffset = geometry.frame(in: .named("outer")).minY - stackPadding
|
||||
let offset = min(max(-scrollOffset, 0), geometry.size.height - 50 - 8)
|
||||
// using named coordinate spaces produces an incorrect scroll offset on iOS 13,
|
||||
// so simply compare the geometry inside and outside the scroll view in the global coordinate space
|
||||
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)
|
||||
.offset(x: 0, y: offset)
|
||||
}
|
||||
|
|
|
@ -12,10 +12,8 @@ import Combine
|
|||
|
||||
struct ComposeView: View {
|
||||
@ObservedObject var draft: Draft
|
||||
let vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
||||
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
@State var viewControllerWidth: CGFloat = 0
|
||||
@EnvironmentObject var uiState: ComposeUIState
|
||||
@State var isPosting = false
|
||||
@State var postProgress: Double = 0
|
||||
|
@ -25,12 +23,8 @@ struct ComposeView: View {
|
|||
|
||||
private let stackPadding: CGFloat = 8
|
||||
|
||||
init(
|
||||
draft: Draft,
|
||||
vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
||||
) {
|
||||
init(draft: Draft) {
|
||||
self.draft = draft
|
||||
self.vcWidthSubject = vcWidthSubject
|
||||
}
|
||||
|
||||
var charactersRemaining: Int {
|
||||
|
@ -63,17 +57,17 @@ struct ComposeView: View {
|
|||
|
||||
var mostOfTheBody: some View {
|
||||
ZStack(alignment: .top) {
|
||||
ScrollView(.vertical) {
|
||||
mainStack
|
||||
GeometryReader { (outer) in
|
||||
ScrollView(.vertical) {
|
||||
mainStack(outerMinY: outer.frame(in: .global).minY)
|
||||
}
|
||||
}
|
||||
|
||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||
}
|
||||
.coordinateSpace(name: "outer")
|
||||
.onAppear(perform: self.didAppear)
|
||||
.navigationBarTitle("Compose")
|
||||
.onReceive(vcWidthSubject) { self.viewControllerWidth = $0 }
|
||||
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
||||
.alert(isPresented: $isShowingPostErrorAlert) {
|
||||
Alert(
|
||||
|
@ -84,14 +78,14 @@ struct ComposeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
var mainStack: some View {
|
||||
func mainStack(outerMinY: CGFloat) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let id = draft.inReplyToID,
|
||||
let status = mastodonController.persistentContainer.status(for: id) {
|
||||
ComposeReplyView(
|
||||
status: status,
|
||||
maxWidth: viewControllerWidth - (2 * stackPadding),
|
||||
stackPadding: stackPadding
|
||||
stackPadding: stackPadding,
|
||||
outerMinY: outerMinY
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -151,7 +145,13 @@ struct ComposeView: View {
|
|||
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
|
||||
uiState.delegate?.dismissCompose()
|
||||
} else {
|
||||
uiState.isShowingSaveDraftSheet = true
|
||||
// if the draft doesn't have content, it doesn't need to be saved
|
||||
if draft.hasContent {
|
||||
uiState.isShowingSaveDraftSheet = true
|
||||
} else {
|
||||
DraftsManager.shared.remove(draft)
|
||||
uiState.delegate?.dismissCompose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,14 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||
context.coordinator.textView = textView
|
||||
|
||||
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: #selector(ComposeHostingController.visibilityButtonPressed(_:)))
|
||||
let visibilityAction: Selector?
|
||||
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()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
toolbar.items = [
|
||||
|
@ -118,16 +125,33 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
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) {
|
||||
uiView.text = text
|
||||
visibilityButton?.image = UIImage(systemName: visibility.imageName)
|
||||
if let visibilityButton = visibilityButton {
|
||||
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
||||
updateVisibilityMenu(visibilityButton)
|
||||
}
|
||||
context.coordinator.text = $text
|
||||
context.coordinator.didChange = textDidChange
|
||||
context.coordinator.uiState = uiState
|
||||
|
||||
if becomeFirstResponder {
|
||||
uiView.becomeFirstResponder()
|
||||
DispatchQueue.main.async {
|
||||
// calling becomeFirstResponder during the SwiftUI update causes a crash on iOS 13
|
||||
uiView.becomeFirstResponder()
|
||||
// can't update @State vars during the SwiftUI update
|
||||
becomeFirstResponder = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,16 @@ class ProfileViewController: UIPageViewController {
|
|||
|
||||
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.delegate = self
|
||||
|
||||
|
@ -162,6 +172,22 @@ 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 {
|
||||
|
|
|
@ -79,8 +79,7 @@ extension TuskerNavigationDelegate {
|
|||
show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
||||
}
|
||||
|
||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||
func compose(editing draft: Draft) {
|
||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||
|
||||
let vc = UINavigationController(rootViewController: compose)
|
||||
|
@ -88,6 +87,11 @@ extension TuskerNavigationDelegate {
|
|||
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 {
|
||||
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
|
||||
vc.animationSourceView = sourceView
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// 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()
|
||||
// }
|
||||
//}
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<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">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17120"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.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">
|
||||
<rect key="frame" x="0.0" y="188.5" width="142" height="18"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bs-Vjq">
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bs-Vjq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="75" height="18"/>
|
||||
<constraints>
|
||||
<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"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB">
|
||||
<rect key="frame" x="83" y="0.0" width="59" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="18" id="k0P-W7-wMF"/>
|
||||
|
@ -247,7 +247,7 @@
|
|||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="104"/>
|
||||
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
|
||||
<image name="chevron.down" catalog="system" width="128" height="72"/>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<image name="globe" catalog="system" width="128" height="121"/>
|
||||
|
|
Loading…
Reference in New Issue