forked from shadowfacts/Tusker
Refactor ComposeView to use a single List for everything
This commit is contained in:
parent
5a5c67e445
commit
7600954f4b
|
@ -152,6 +152,7 @@
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||||
|
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */; };
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
||||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284724ECBCB100C732D3 /* ComposeView.swift */; };
|
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284724ECBCB100C732D3 /* ComposeView.swift */; };
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */; };
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */; };
|
||||||
|
@ -507,6 +508,7 @@
|
||||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPicker.swift; sourceTree = "<group>"; };
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
|
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
|
||||||
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
D677284724ECBCB100C732D3 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||||
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; };
|
D677284924ECBDF400C732D3 /* ComposeCurrentAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCurrentAccount.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1289,6 +1291,7 @@
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */,
|
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */,
|
||||||
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */,
|
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */,
|
||||||
|
D673ACCD2919E74200D6F8B0 /* MenuPicker.swift */,
|
||||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||||
|
@ -1853,6 +1856,7 @@
|
||||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||||
|
D673ACCE2919E74200D6F8B0 /* MenuPicker.swift in Sources */,
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
||||||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
||||||
|
|
|
@ -14,7 +14,6 @@ import Vision
|
||||||
struct ComposeAttachmentRow: View {
|
struct ComposeAttachmentRow: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@ObservedObject var attachment: CompositionAttachment
|
@ObservedObject var attachment: CompositionAttachment
|
||||||
let heightChanged: (CGFloat) -> Void
|
|
||||||
|
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
@State private var mode: Mode = .allowEntry
|
@State private var mode: Mode = .allowEntry
|
||||||
|
@ -47,7 +46,6 @@ struct ComposeAttachmentRow: View {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .allowEntry:
|
case .allowEntry:
|
||||||
ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80)
|
ComposeTextView(text: $attachment.attachmentDescription, placeholder: Text("Describe for the visually impaired…"), minHeight: 80)
|
||||||
.heightDidChange(self.heightChanged)
|
|
||||||
.backgroundColor(.clear)
|
.backgroundColor(.clear)
|
||||||
|
|
||||||
case .recognizingText:
|
case .recognizingText:
|
||||||
|
|
|
@ -18,23 +18,17 @@ struct ComposeAttachmentsList: View {
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
@State var isShowingAssetPickerPopover = false
|
@State var isShowingAssetPickerPopover = false
|
||||||
@State var isShowingCreateDrawing = false
|
@State var isShowingCreateDrawing = false
|
||||||
@State var rowHeights = [UUID: CGFloat]()
|
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
Group {
|
||||||
ForEach(draft.attachments) { (attachment) in
|
ForEach(draft.attachments) { (attachment) in
|
||||||
ComposeAttachmentRow(
|
ComposeAttachmentRow(
|
||||||
draft: draft,
|
draft: draft,
|
||||||
attachment: attachment
|
attachment: attachment
|
||||||
) { (newHeight) in
|
)
|
||||||
// in case height changed callback is called after atachment is removed but before view hierarchy is updated
|
|
||||||
if draft.attachments.contains(where: { $0.id == attachment.id }) {
|
|
||||||
rowHeights[attachment.id] = newHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||||
.onDrag { NSItemProvider(object: attachment) }
|
.onDrag { NSItemProvider(object: attachment) }
|
||||||
}
|
}
|
||||||
|
@ -69,12 +63,7 @@ struct ComposeAttachmentsList: View {
|
||||||
.frame(height: cellHeight / 2)
|
.frame(height: cellHeight / 2)
|
||||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||||
}
|
}
|
||||||
.listStyle(PlainListStyle())
|
|
||||||
// todo: scrollDisabled doesn't remove the need for manually calculating the frame height
|
|
||||||
.frame(height: totalListHeight)
|
|
||||||
.scrollDisabledIfAvailable(totalHeight: totalListHeight)
|
|
||||||
.onAppear(perform: self.didAppear)
|
.onAppear(perform: self.didAppear)
|
||||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addButtonImageName: String {
|
private var addButtonImageName: String {
|
||||||
|
@ -104,13 +93,6 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var totalListHeight: CGFloat {
|
|
||||||
let totalRowHeights = rowHeights.values.reduce(0, +)
|
|
||||||
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
|
|
||||||
let addButtonHeight = 3 * (cellHeight / 2 + cellPadding)
|
|
||||||
return totalRowHeights + totalPadding + addButtonHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
private func didAppear() {
|
private func didAppear() {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
// these appearance proxy hacks are no longer necessary
|
// these appearance proxy hacks are no longer necessary
|
||||||
|
@ -122,17 +104,6 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
|
||||||
var copy = rowHeights
|
|
||||||
for k in copy.keys where !attachments.contains(where: { k == $0.id }) {
|
|
||||||
copy.removeValue(forKey: k)
|
|
||||||
}
|
|
||||||
for attachment in attachments where !copy.keys.contains(attachment.id) {
|
|
||||||
copy[attachment.id] = cellHeight
|
|
||||||
}
|
|
||||||
self.rowHeights = copy
|
|
||||||
}
|
|
||||||
|
|
||||||
private func assetPickerPopover() -> some View {
|
private func assetPickerPopover() -> some View {
|
||||||
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
|
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
@ -214,16 +185,6 @@ fileprivate extension View {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func scrollDisabledIfAvailable(totalHeight: CGFloat) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.scrollDisabled(true)
|
|
||||||
} else {
|
|
||||||
self.frame(height: totalHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
|
|
|
@ -428,6 +428,10 @@ extension ComposeHostingController: UIAdaptivePresentationControllerDelegate {
|
||||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||||
return Preferences.shared.automaticallySaveDrafts || !draft.hasContent
|
return Preferences.shared.automaticallySaveDrafts || !draft.hasContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: self, for: nil)
|
||||||
|
}
|
||||||
|
|
||||||
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
|
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
|
||||||
uiState.isShowingSaveDraftSheet = true
|
uiState.isShowingSaveDraftSheet = true
|
||||||
|
|
|
@ -44,6 +44,7 @@ struct ComposePollView: View {
|
||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.accentColor(buttonForegroundColor)
|
.accentColor(buttonForegroundColor)
|
||||||
.background(Circle().foregroundColor(buttonBackgroundColor))
|
.background(Circle().foregroundColor(buttonBackgroundColor))
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
|
@ -52,31 +53,22 @@ struct ComposePollView: View {
|
||||||
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
||||||
ComposePollOption(poll: poll, option: e.element, optionIndex: e.offset)
|
ComposePollOption(poll: poll, option: e.element, optionIndex: e.offset)
|
||||||
}
|
}
|
||||||
.transition(.slide)
|
|
||||||
|
|
||||||
Button(action: self.addOption) {
|
Button(action: self.addOption) {
|
||||||
Label("Add Option", systemImage: "plus")
|
Label("Add Option", systemImage: "plus")
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
// use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes
|
MenuPicker(selection: $poll.multiple, options: [
|
||||||
// this is deprecated in iOS 15, but using .animation(nil, value: poll.multiple) does not work (it still animates)
|
.init(title: "Allow multiple", value: true),
|
||||||
// nor does setting that on the Text rather than the Picker
|
.init(title: "Single choice", value: false),
|
||||||
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choice" : "Single choice")) {
|
])
|
||||||
Text("Allow multiple choices").tag(true)
|
|
||||||
Text("Single choice").tag(false)
|
|
||||||
}
|
|
||||||
.animation(nil)
|
|
||||||
.pickerStyle(MenuPickerStyle())
|
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
Picker(selection: $duration, label: Text(verbatim: ComposePollView.formatter.string(from: duration.timeInterval)!)) {
|
MenuPicker(selection: $duration, options: Duration.allCases.map {
|
||||||
ForEach(Duration.allCases, id: \.self) { (duration) in
|
.init(title: ComposePollView.formatter.string(from: $0.timeInterval)!, value: $0)
|
||||||
Text(ComposePollView.formatter.string(from: duration.timeInterval)!).tag(duration)
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
.animation(nil)
|
|
||||||
.pickerStyle(MenuPickerStyle())
|
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,9 +102,7 @@ struct ComposePollView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addOption() {
|
private func addOption() {
|
||||||
withAnimation(.easeInOut(duration: 0.25)) {
|
poll.options.append(Draft.Poll.Option(""))
|
||||||
poll.options.append(Draft.Poll.Option(""))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +153,7 @@ struct ComposePollOption: View {
|
||||||
Button(action: self.removeOption) {
|
Button(action: self.removeOption) {
|
||||||
Image(systemName: "minus.circle.fill")
|
Image(systemName: "minus.circle.fill")
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
||||||
.disabled(poll.options.count == 1)
|
.disabled(poll.options.count == 1)
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
|
@ -175,9 +166,7 @@ struct ComposePollOption: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeOption() {
|
private func removeOption() {
|
||||||
_ = withAnimation {
|
poll.options.remove(at: optionIndex)
|
||||||
poll.options.remove(at: optionIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Checkbox: View {
|
struct Checkbox: View {
|
||||||
|
|
|
@ -17,18 +17,20 @@ struct ComposeReplyContentView: UIViewRepresentable {
|
||||||
|
|
||||||
let heightChanged: (CGFloat) -> Void
|
let heightChanged: (CGFloat) -> Void
|
||||||
|
|
||||||
func makeUIView(context: Context) -> ComposeReplyContentTextView {
|
func makeUIView(context: Context) -> UIViewType {
|
||||||
let view = ComposeReplyContentTextView()
|
let view = ComposeReplyContentTextView()
|
||||||
view.overrideMastodonController = mastodonController
|
view.overrideMastodonController = mastodonController
|
||||||
view.setTextFrom(status: status)
|
view.setTextFrom(status: status)
|
||||||
view.isUserInteractionEnabled = false
|
view.isUserInteractionEnabled = false
|
||||||
|
// scroll needs to be enabled, otherwise the text view never reports a contentSize greater than 1 line
|
||||||
|
view.isScrollEnabled = true
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .clear
|
||||||
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: ComposeReplyContentTextView, context: Context) {
|
func updateUIView(_ uiView: UIViewType, context: Context) {
|
||||||
uiView.heightChanged = heightChanged
|
uiView.heightChanged = heightChanged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ import SwiftUI
|
||||||
|
|
||||||
struct ComposeReplyView: View {
|
struct ComposeReplyView: View {
|
||||||
let status: StatusMO
|
let status: StatusMO
|
||||||
let stackPadding: CGFloat
|
let rowTopInset: CGFloat
|
||||||
|
let globalFrameOutsideList: CGRect
|
||||||
|
|
||||||
@State private var displayNameHeight: CGFloat?
|
@State private var displayNameHeight: CGFloat?
|
||||||
@State private var contentHeight: CGFloat?
|
@State private var contentHeight: CGFloat?
|
||||||
|
@ -44,9 +45,13 @@ struct ComposeReplyView: View {
|
||||||
displayNameHeight = newValue
|
displayNameHeight = newValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ComposeReplyContentView(status: status) { newHeight in
|
ComposeReplyContentView(status: status) { newHeight in
|
||||||
contentHeight = newHeight
|
// otherwise, with long in-reply-to statuses, the main content text view position seems not to update
|
||||||
|
// and it ends up partially behind the header
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
contentHeight = newHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(height: contentHeight ?? 0)
|
.frame(height: contentHeight ?? 0)
|
||||||
}
|
}
|
||||||
|
@ -55,20 +60,22 @@ struct ComposeReplyView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||||
let scrollOffset = -geometry.frame(in: .named(ComposeView.coordinateSpaceOutsideOfScrollView)).minY
|
// using a coordinate space declared outside of the List doesn't work, so we do the math ourselves
|
||||||
|
let globalFrame = geometry.frame(in: .global)
|
||||||
|
let scrollOffset = -(globalFrame.minY - globalFrameOutsideList.minY)
|
||||||
|
|
||||||
|
// add rowTopInset so that the image is always at least rowTopInset away from the top
|
||||||
|
var offset = scrollOffset + rowTopInset
|
||||||
|
|
||||||
// add stackPadding so that the image is always at least stackPadding away from the top
|
|
||||||
var offset = scrollOffset + stackPadding
|
|
||||||
|
|
||||||
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
|
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
|
||||||
offset = max(offset, 0)
|
offset = max(offset, 0)
|
||||||
|
|
||||||
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
|
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
|
||||||
let maxOffset = max((contentHeight ?? 0) + (displayNameHeight ?? 0) - 50, 0)
|
let maxOffset = max((contentHeight ?? 0) + (displayNameHeight ?? 0) - 50, 0)
|
||||||
|
|
||||||
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
|
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
|
||||||
offset = min(offset, maxOffset)
|
offset = min(offset, maxOffset)
|
||||||
|
|
||||||
return ComposeAvatarImageView(url: status.account.avatar)
|
return ComposeAvatarImageView(url: status.account.avatar)
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||||
|
|
|
@ -42,12 +42,12 @@ import Combine
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ComposeView: View {
|
struct ComposeView: View {
|
||||||
static let coordinateSpaceOutsideOfScrollView = "coordinateSpaceOutsideOfScrollView"
|
|
||||||
|
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
|
|
||||||
|
@State private var globalFrameOutsideList: CGRect = .zero
|
||||||
|
|
||||||
@OptionalStateObject private var poster: PostService?
|
@OptionalStateObject private var poster: PostService?
|
||||||
@State private var isShowingPostErrorAlert = false
|
@State private var isShowingPostErrorAlert = false
|
||||||
@State private var postError: PostService.Error?
|
@State private var postError: PostService.Error?
|
||||||
|
@ -80,11 +80,8 @@ struct ComposeView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ScrollView(.vertical) {
|
mainList
|
||||||
mainStack
|
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
||||||
}
|
|
||||||
.coordinateSpace(name: ComposeView.coordinateSpaceOutsideOfScrollView)
|
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
|
||||||
|
|
||||||
if let poster = poster {
|
if let poster = poster {
|
||||||
// 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
|
||||||
|
@ -93,6 +90,13 @@ struct ComposeView: View {
|
||||||
|
|
||||||
autocompleteSuggestions
|
autocompleteSuggestions
|
||||||
}
|
}
|
||||||
|
.background(GeometryReader { proxy in
|
||||||
|
Color.clear
|
||||||
|
.preference(key: GlobalFrameOutsideListPrefKey.self, value: proxy.frame(in: .global))
|
||||||
|
.onPreferenceChange(GlobalFrameOutsideListPrefKey.self) { frame in
|
||||||
|
globalFrameOutsideList = frame
|
||||||
|
}
|
||||||
|
})
|
||||||
.navigationBarTitle("Compose")
|
.navigationBarTitle("Compose")
|
||||||
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
.actionSheet(isPresented: $uiState.isShowingSaveDraftSheet, content: self.saveAndCloseSheet)
|
||||||
.alert(isPresented: $isShowingPostErrorAlert) {
|
.alert(isPresented: $isShowingPostErrorAlert) {
|
||||||
|
@ -120,41 +124,49 @@ struct ComposeView: View {
|
||||||
.animation(.default, value: uiState.autocompleteState)
|
.animation(.default, value: uiState.autocompleteState)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainStack: some View {
|
var mainList: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
List {
|
||||||
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
|
rowTopInset: 8,
|
||||||
|
globalFrameOutsideList: globalFrameOutsideList
|
||||||
)
|
)
|
||||||
|
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
header
|
header
|
||||||
|
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
if draft.contentWarningEnabled {
|
if draft.contentWarningEnabled {
|
||||||
ComposeEmojiTextField(text: $draft.contentWarning, placeholder: "Write your warning here")
|
ComposeEmojiTextField(text: $draft.contentWarning, placeholder: "Write your warning here")
|
||||||
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
MainComposeTextView(
|
MainComposeTextView(draft: draft)
|
||||||
draft: draft
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
)
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
if let poll = draft.poll {
|
if let poll = draft.poll {
|
||||||
ComposePollView(draft: draft, poll: poll)
|
ComposePollView(draft: draft, poll: poll)
|
||||||
.transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing))))
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeAttachmentsList(
|
ComposeAttachmentsList(
|
||||||
draft: draft
|
draft: draft
|
||||||
)
|
)
|
||||||
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
|
||||||
.padding([.top, .bottom], -8)
|
|
||||||
}
|
}
|
||||||
|
.animation(.default, value: draft.poll?.options.count)
|
||||||
|
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
||||||
|
.listStyle(.plain)
|
||||||
.disabled(isPosting)
|
.disabled(isPosting)
|
||||||
.padding(stackPadding)
|
.padding(.bottom, uiState.autocompleteState != nil ? 46 : 0)
|
||||||
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
|
@ -258,6 +270,13 @@ private extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct GlobalFrameOutsideListPrefKey: PreferenceKey {
|
||||||
|
static var defaultValue: CGRect = .zero
|
||||||
|
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
|
||||||
|
value = nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//struct ComposeView_Previews: PreviewProvider {
|
//struct ComposeView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
// ComposeView()
|
// ComposeView()
|
||||||
|
|
|
@ -17,6 +17,10 @@ struct MainComposeTextView: View {
|
||||||
if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
|
if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
|
||||||
return Text("Happy π day!")
|
return Text("Happy π day!")
|
||||||
}
|
}
|
||||||
|
} else if components.month == 9 && components.day == 5 {
|
||||||
|
// https://weirder.earth/@noracodes/109276419847254552
|
||||||
|
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
|
||||||
|
return Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
|
||||||
} else if components.month == 9 && components.day == 21 {
|
} else if components.month == 9 && components.day == 21 {
|
||||||
return Text("Do you remember?")
|
return Text("Do you remember?")
|
||||||
} else if components.month == 10 && components.day == 31 {
|
} else if components.month == 10 && components.day == 31 {
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
//
|
||||||
|
// MenuPicker.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/7/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MenuPicker<Value: Hashable>: View {
|
||||||
|
@Binding var selection: Value
|
||||||
|
let options: [Option]
|
||||||
|
|
||||||
|
private var selectedOption: Option {
|
||||||
|
options.first(where: { $0.value == selection })!
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach(options, id: \.value) { option in
|
||||||
|
Button {
|
||||||
|
selection = option.value
|
||||||
|
} label: {
|
||||||
|
Label(option.title, systemImage: selection == option.value ? "checkmark" : "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
// zstack so that the size of the picker is the size of the largest option
|
||||||
|
ZStack {
|
||||||
|
ForEach(options, id: \.value) { option in
|
||||||
|
HStack {
|
||||||
|
Text(option.title)
|
||||||
|
Image(systemName: "chevron.up.chevron.down")
|
||||||
|
}
|
||||||
|
.opacity(option.value == selection ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menuStyle(.borderlessButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Option {
|
||||||
|
let title: String
|
||||||
|
let value: Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MenuPicker_Previews: PreviewProvider {
|
||||||
|
@State static var value = 0
|
||||||
|
static var previews: some View {
|
||||||
|
MenuPicker(selection: $value, options: [
|
||||||
|
.init(title: "Zero", value: 0),
|
||||||
|
.init(title: "One", value: 1),
|
||||||
|
.init(title: "Two", value: 2),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue