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 */; };
|
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, ); }; };
|
||||||
|
@ -547,6 +548,7 @@
|
||||||
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>"; };
|
||||||
|
@ -1251,6 +1253,7 @@
|
||||||
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 */,
|
||||||
|
@ -1794,6 +1797,7 @@
|
||||||
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 */,
|
||||||
|
|
|
@ -124,9 +124,19 @@ 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
|
||||||
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 {
|
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 {
|
} else {
|
||||||
self.reblog = nil
|
self.reblog = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,45 +44,50 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: self.addAttachment) {
|
Button(action: self.addAttachment) {
|
||||||
HStack {
|
if #available(iOS 14.0, *) {
|
||||||
addButtonImage
|
Label("Add photo or video", systemImage: addButtonImageName)
|
||||||
Text("Add image or video")
|
} else {
|
||||||
}
|
HStack {
|
||||||
}
|
Image(systemName: addButtonImageName)
|
||||||
.foregroundColor(.blue)
|
Text("Add photo or video")
|
||||||
.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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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)
|
.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 addButtonImage: Image {
|
private var addButtonImageName: String {
|
||||||
let name: String
|
|
||||||
switch colorScheme {
|
switch colorScheme {
|
||||||
case .dark:
|
case .dark:
|
||||||
name = "photo.fill"
|
return "photo.fill"
|
||||||
case .light:
|
case .light:
|
||||||
name = "photo"
|
return "photo"
|
||||||
@unknown default:
|
@unknown default:
|
||||||
name = "photo"
|
return "photo"
|
||||||
}
|
}
|
||||||
return Image(systemName: name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canAddAttachment: Bool {
|
private var canAddAttachment: Bool {
|
||||||
|
@ -90,15 +95,14 @@ struct ComposeAttachmentsList: View {
|
||||||
case .pleroma:
|
case .pleroma:
|
||||||
return true
|
return true
|
||||||
case .mastodon:
|
case .mastodon:
|
||||||
// todo: this technically allows invalid image/video combinations
|
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image }
|
||||||
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
|
let addButtonHeight = cellHeight + cellPadding * 2
|
||||||
return totalRowHeights + totalPadding + addButtonHeight
|
return totalRowHeights + totalPadding + addButtonHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,24 +11,18 @@ 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(
|
ComposeView(draft: uiState.draft)
|
||||||
draft: uiState.draft,
|
|
||||||
vcWidthSubject: vcWidthSubject
|
|
||||||
)
|
|
||||||
.environmentObject(mastodonController)
|
.environmentObject(mastodonController)
|
||||||
.environmentObject(uiState)
|
.environmentObject(uiState)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,7 @@ struct ComposeCurrentAccount: View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
ComposeAvatarImageView(url: account.avatar)
|
ComposeAvatarImageView(url: account.avatar)
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(verbatim: account.displayName)
|
AccountDisplayNameLabel(account: mastodonController.persistentContainer.account(for: account.id)!, fontSize: 20)
|
||||||
.font(.system(size: 20, weight: .semibold))
|
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Text(verbatim: "@\(account.acct)")
|
Text(verbatim: "@\(account.acct)")
|
||||||
|
|
|
@ -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,7 +42,6 @@ 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)
|
||||||
|
@ -58,7 +57,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
|
||||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 44, right: 0)
|
updateAdditionalSafeAreaInsets()
|
||||||
|
|
||||||
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||||
|
|
||||||
|
@ -82,12 +81,6 @@ 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)
|
||||||
|
|
||||||
|
@ -116,9 +109,15 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
let toolbar = UIToolbar()
|
let toolbar = UIToolbar()
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: #selector(visibilityButtonPressed))
|
let visibilityAction: Selector?
|
||||||
visibilityItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
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)
|
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)),
|
||||||
|
@ -129,6 +128,9 @@ 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)
|
||||||
|
@ -139,6 +141,17 @@ 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) {
|
||||||
|
@ -171,6 +184,13 @@ 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) {
|
||||||
|
@ -185,6 +205,15 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +223,8 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
case .pleroma:
|
case .pleroma:
|
||||||
return true
|
return true
|
||||||
case .mastodon:
|
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
|
return itemProviders.count + draft.attachments.count <= 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,12 +247,15 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func visibilityButtonPressed(_ sender: UIBarButtonItem) {
|
@objc func visibilityButtonPressed(_ sender: UIBarButtonItem) {
|
||||||
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
|
// if #available(iOS 14.0, *) {
|
||||||
guard let visibility = visibility else { return }
|
// } else {
|
||||||
self.draft.visibility = visibility
|
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
|
||||||
}
|
guard let visibility = visibility else { return }
|
||||||
alertController.popoverPresentationController?.barButtonItem = sender
|
self.draft.visibility = visibility
|
||||||
present(alertController, animated: true)
|
}
|
||||||
|
alertController.popoverPresentationController?.barButtonItem = sender
|
||||||
|
present(alertController, animated: true)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func draftsButtonPresed() {
|
@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 {
|
extension ComposeHostingController: ComposeUIStateDelegate {
|
||||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }
|
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }
|
||||||
|
|
||||||
|
|
|
@ -12,37 +12,33 @@ 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.constraint.constant = maxWidth
|
uiView.heightChanged = heightChanged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComposeReplyContentTextView: StatusContentTextView {
|
class ComposeReplyContentTextView: StatusContentTextView {
|
||||||
|
var heightChanged: ((CGFloat) -> Void)?
|
||||||
|
|
||||||
var maxWidth: CGFloat!
|
override func layoutSubviews() {
|
||||||
var constraint: NSLayoutConstraint!
|
super.layoutSubviews()
|
||||||
|
|
||||||
override func didMoveToSuperview() {
|
|
||||||
super.didMoveToSuperview()
|
|
||||||
|
|
||||||
translatesAutoresizingMaskIntoConstraints = false
|
heightChanged?(contentSize.height)
|
||||||
constraint = widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth)
|
|
||||||
constraint.isActive = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,10 @@ 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
|
||||||
|
|
||||||
|
@ -22,8 +24,7 @@ struct ComposeReplyView: View {
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(verbatim: status.account.displayName)
|
AccountDisplayNameLabel(account: status.account, fontSize: 17)
|
||||||
.font(.system(size: 17, weight: .semibold))
|
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
|
|
||||||
|
@ -35,7 +36,10 @@ struct ComposeReplyView: View {
|
||||||
Spacer()
|
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)
|
.offset(x: -4, y: -8)
|
||||||
.padding(.bottom, -8)
|
.padding(.bottom, -8)
|
||||||
}
|
}
|
||||||
|
@ -45,8 +49,11 @@ struct ComposeReplyView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||||
let scrollOffset = geometry.frame(in: .named("outer")).minY - stackPadding
|
// using named coordinate spaces produces an incorrect scroll offset on iOS 13,
|
||||||
let offset = min(max(-scrollOffset, 0), geometry.size.height - 50 - 8)
|
// 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)
|
return ComposeAvatarImageView(url: status.account.avatar)
|
||||||
.offset(x: 0, y: offset)
|
.offset(x: 0, y: offset)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,8 @@ 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
|
||||||
|
@ -25,12 +23,8 @@ struct ComposeView: View {
|
||||||
|
|
||||||
private let stackPadding: CGFloat = 8
|
private let stackPadding: CGFloat = 8
|
||||||
|
|
||||||
init(
|
init(draft: Draft) {
|
||||||
draft: Draft,
|
|
||||||
vcWidthSubject: PassthroughSubject<CGFloat, Never>
|
|
||||||
) {
|
|
||||||
self.draft = draft
|
self.draft = draft
|
||||||
self.vcWidthSubject = vcWidthSubject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var charactersRemaining: Int {
|
var charactersRemaining: Int {
|
||||||
|
@ -63,17 +57,17 @@ struct ComposeView: View {
|
||||||
|
|
||||||
var mostOfTheBody: some View {
|
var mostOfTheBody: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ScrollView(.vertical) {
|
GeometryReader { (outer) in
|
||||||
mainStack
|
ScrollView(.vertical) {
|
||||||
|
mainStack(outerMinY: outer.frame(in: .global).minY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
||||||
|
@ -84,14 +78,14 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainStack: some View {
|
func mainStack(outerMinY: CGFloat) -> 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,
|
||||||
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
|
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
|
||||||
uiState.delegate?.dismissCompose()
|
uiState.delegate?.dismissCompose()
|
||||||
} else {
|
} 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
|
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||||
context.coordinator.textView = textView
|
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()
|
let toolbar = UIToolbar()
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
toolbar.items = [
|
toolbar.items = [
|
||||||
|
@ -118,16 +125,33 @@ 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
|
||||||
visibilityButton?.image = UIImage(systemName: visibility.imageName)
|
if let visibilityButton = visibilityButton {
|
||||||
|
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 {
|
||||||
uiView.becomeFirstResponder()
|
|
||||||
DispatchQueue.main.async {
|
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
|
becomeFirstResponder = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,16 @@ 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
|
||||||
|
|
||||||
|
@ -161,6 +171,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,7 @@ extension TuskerNavigationDelegate {
|
||||||
show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
func compose(editing draft: Draft) {
|
||||||
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)
|
||||||
|
@ -88,6 +87,11 @@ 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
|
||||||
|
|
|
@ -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"?>
|
<?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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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="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" 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"/>
|
<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" 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"/>
|
<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="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="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"/>
|
||||||
|
|
Loading…
Reference in New Issue