2024-01-02 14:23:36 +01:00
|
|
|
//Made by Lumaa
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
import UIKit
|
|
|
|
import PhotosUI
|
|
|
|
|
|
|
|
struct PostingView: View {
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@Environment(AccountManager.self) private var accountManager: AccountManager
|
2024-01-04 22:19:35 +01:00
|
|
|
@Environment(Navigator.self) private var navigator: Navigator
|
2024-01-02 14:23:36 +01:00
|
|
|
|
2024-01-06 03:36:26 +01:00
|
|
|
public var initialString: String = ""
|
2024-01-10 17:45:41 +01:00
|
|
|
public var replyId: String? = nil
|
2024-01-22 06:48:38 +01:00
|
|
|
public var editId: String? = nil
|
2024-01-06 03:36:26 +01:00
|
|
|
|
|
|
|
@State private var viewModel: PostingView.ViewModel = PostingView.ViewModel()
|
|
|
|
|
2024-01-27 08:55:58 +01:00
|
|
|
@State private var hasKeyboard: Bool = true
|
2024-01-02 14:23:36 +01:00
|
|
|
@State private var visibility: Visibility = .pub
|
|
|
|
@State private var selectedPhotos: PhotosPickerItem?
|
2024-01-26 14:52:11 +01:00
|
|
|
@State private var selectingEmoji: Bool = false
|
2024-01-02 14:23:36 +01:00
|
|
|
|
|
|
|
@State private var postingStatus: Bool = false
|
|
|
|
|
|
|
|
var body: some View {
|
2024-01-26 22:58:57 +01:00
|
|
|
|
2024-01-02 14:23:36 +01:00
|
|
|
if accountManager.getAccount() != nil {
|
|
|
|
posting
|
2024-01-26 22:58:57 +01:00
|
|
|
.background(Color.appBackground)
|
2024-01-26 14:52:11 +01:00
|
|
|
.sheet(isPresented: $selectingEmoji) {
|
2024-01-26 22:58:57 +01:00
|
|
|
EmojiSelector(viewModel: $viewModel)
|
|
|
|
.presentationDetents([.height(200), .medium])
|
2024-01-26 14:52:11 +01:00
|
|
|
.presentationDragIndicator(.visible)
|
2024-01-26 22:58:57 +01:00
|
|
|
.presentationBackgroundInteraction(.enabled(upThrough: .height(200))) // Allow users to move the cursor while adding emojis
|
2024-01-26 14:52:11 +01:00
|
|
|
}
|
2024-01-02 14:23:36 +01:00
|
|
|
} else {
|
|
|
|
loading
|
2024-01-26 22:58:57 +01:00
|
|
|
.background(Color.appBackground)
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var posting: some View {
|
|
|
|
VStack {
|
|
|
|
HStack(alignment: .top, spacing: 0) {
|
|
|
|
// MARK: Profile picture
|
|
|
|
profilePicture
|
|
|
|
|
|
|
|
VStack(alignment: .leading) {
|
|
|
|
// MARK: Status main content
|
|
|
|
VStack(alignment: .leading, spacing: 10) {
|
2024-01-24 19:40:14 +01:00
|
|
|
Text("@\(accountManager.forceAccount().username)")
|
2024-01-02 14:23:36 +01:00
|
|
|
.multilineTextAlignment(.leading)
|
|
|
|
.bold()
|
|
|
|
|
2024-01-06 03:36:26 +01:00
|
|
|
DynamicTextEditor($viewModel.postText, getTextView: { textView in
|
|
|
|
viewModel.textView = textView
|
|
|
|
})
|
2024-01-02 14:23:36 +01:00
|
|
|
.placeholder(String(localized: "status.posting.placeholder"))
|
2024-01-10 17:45:41 +01:00
|
|
|
.setKeyboardType(.twitter)
|
2024-01-27 08:55:58 +01:00
|
|
|
.onFocus {
|
|
|
|
selectingEmoji = false
|
|
|
|
}
|
2024-01-02 14:23:36 +01:00
|
|
|
.multilineTextAlignment(.leading)
|
|
|
|
.font(.callout)
|
|
|
|
.foregroundStyle(Color(uiColor: UIColor.label))
|
|
|
|
|
|
|
|
editorButtons
|
|
|
|
.padding(.vertical)
|
|
|
|
}
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
}
|
2024-01-27 08:55:58 +01:00
|
|
|
.onChange(of: selectingEmoji) { _, new in
|
|
|
|
guard new == false else { return }
|
|
|
|
viewModel.textView?.becomeFirstResponder()
|
|
|
|
}
|
2024-01-02 14:23:36 +01:00
|
|
|
|
|
|
|
HStack {
|
|
|
|
Picker("status.posting.visibility", selection: $visibility) {
|
|
|
|
ForEach(Visibility.allCases, id: \.self) { item in
|
|
|
|
HStack(alignment: .firstTextBaseline) {
|
|
|
|
switch (item) {
|
|
|
|
case .pub:
|
|
|
|
Text("status.posting.visibility.public")
|
|
|
|
.foregroundStyle(Color.gray)
|
|
|
|
case .unlisted:
|
|
|
|
Text("status.posting.visibility.unlisted")
|
|
|
|
.foregroundStyle(Color.gray)
|
|
|
|
case .direct:
|
|
|
|
Text("status.posting.visibility.direct")
|
|
|
|
.foregroundStyle(Color.gray)
|
|
|
|
case .priv:
|
|
|
|
Text("status.posting.visibility.private")
|
|
|
|
.foregroundStyle(Color.gray)
|
|
|
|
}
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.labelsHidden()
|
|
|
|
.pickerStyle(.menu)
|
|
|
|
.foregroundStyle(Color.gray)
|
|
|
|
.frame(width: 200, alignment: .leading)
|
|
|
|
.multilineTextAlignment(.leading)
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
Button {
|
|
|
|
Task {
|
|
|
|
if let client = accountManager.getClient() {
|
|
|
|
postingStatus = true
|
2024-01-22 06:48:38 +01:00
|
|
|
let json: StatusData = .init(status: viewModel.postText.string, visibility: visibility, inReplyToId: replyId)
|
|
|
|
|
|
|
|
let isEdit: Bool = editId != nil
|
|
|
|
let endp: Endpoint = isEdit ? Statuses.editStatus(id: editId!, json: json) : Statuses.postStatus(json: json)
|
|
|
|
|
|
|
|
let newStatus: Status = try await client.post(endpoint: endp)
|
2024-01-02 14:23:36 +01:00
|
|
|
postingStatus = false
|
2024-01-10 17:45:41 +01:00
|
|
|
HapticManager.playHaptics(haptics: Haptic.success)
|
2024-01-02 14:23:36 +01:00
|
|
|
dismiss()
|
2024-01-10 17:45:41 +01:00
|
|
|
navigator.navigate(to: .post(status: newStatus))
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
if postingStatus {
|
|
|
|
ProgressView()
|
|
|
|
.progressViewStyle(.circular)
|
|
|
|
.foregroundStyle(Color.appBackground)
|
2024-01-02 19:49:28 +01:00
|
|
|
.tint(Color.appBackground)
|
2024-01-02 14:23:36 +01:00
|
|
|
} else {
|
|
|
|
Text("status.posting.post")
|
|
|
|
}
|
|
|
|
}
|
2024-01-06 03:36:26 +01:00
|
|
|
.disabled(postingStatus || viewModel.postText.length <= 0)
|
2024-01-24 19:40:14 +01:00
|
|
|
.buttonStyle(LargeButton(filled: true, height: 7.5, disabled: postingStatus || viewModel.postText.length <= 0))
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|
|
|
|
.padding()
|
|
|
|
}
|
|
|
|
.navigationBarBackButtonHidden()
|
2024-01-26 14:10:17 +01:00
|
|
|
.navigationTitle(Text(editId == nil ? "status.posting" : "status.editing"))
|
2024-01-02 14:23:36 +01:00
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
.toolbar {
|
|
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
|
|
Button {
|
|
|
|
dismiss()
|
|
|
|
} label: {
|
|
|
|
Text("status.posting.cancel")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onAppear {
|
2024-01-22 06:48:38 +01:00
|
|
|
if !initialString.isEmpty && editId == nil {
|
|
|
|
viewModel.append(text: initialString + " ") // add space for quick typing
|
|
|
|
} else {
|
|
|
|
viewModel.append(text: initialString) // editing doesn't need quick typing
|
|
|
|
}
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var loading: some View {
|
|
|
|
ProgressView()
|
|
|
|
.foregroundStyle(.white)
|
|
|
|
.progressViewStyle(.circular)
|
|
|
|
}
|
|
|
|
|
|
|
|
var editorButtons: some View {
|
|
|
|
HStack(spacing: 18) {
|
|
|
|
PhotosPicker(selection: $selectedPhotos, matching: .any(of: [.images, .videos]), label: {
|
|
|
|
Image(systemName: "photo.badge.plus")
|
|
|
|
.font(.callout)
|
|
|
|
.foregroundStyle(.gray)
|
|
|
|
})
|
|
|
|
.tint(Color.blue)
|
|
|
|
|
|
|
|
actionButton("number") {
|
|
|
|
DispatchQueue.main.async {
|
2024-01-10 17:45:41 +01:00
|
|
|
viewModel.append(text: "#")
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|
|
|
|
}
|
2024-01-26 14:52:11 +01:00
|
|
|
|
|
|
|
actionButton("face.smiling") {
|
2024-01-27 08:55:58 +01:00
|
|
|
viewModel.textView?.resignFirstResponder()
|
2024-01-26 14:52:11 +01:00
|
|
|
selectingEmoji.toggle()
|
|
|
|
}
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
func actionButton(_ image: String, action: @escaping () -> Void) -> some View {
|
|
|
|
Button {
|
|
|
|
action()
|
|
|
|
} label: {
|
|
|
|
Image(systemName: image)
|
|
|
|
.font(.callout)
|
|
|
|
}
|
|
|
|
.tint(Color.gray)
|
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder
|
|
|
|
func asyncActionButton(_ image: String, action: @escaping () async -> Void) -> some View {
|
|
|
|
Button {
|
|
|
|
Task {
|
|
|
|
await action()
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
Image(systemName: image)
|
|
|
|
.font(.callout)
|
|
|
|
}
|
|
|
|
.tint(Color.gray)
|
|
|
|
}
|
|
|
|
|
|
|
|
var profilePicture: some View {
|
|
|
|
OnlineImage(url: accountManager.forceAccount().avatar, size: 50, useNuke: true)
|
|
|
|
.frame(width: 40, height: 40)
|
|
|
|
.padding(.horizontal)
|
|
|
|
.clipShape(.circle)
|
|
|
|
}
|
2024-01-06 03:36:26 +01:00
|
|
|
|
2024-01-10 17:45:41 +01:00
|
|
|
@Observable public class ViewModel: NSObject {
|
|
|
|
init(text: String = "") {
|
|
|
|
self.postText = NSMutableAttributedString(string: text)
|
2024-01-06 03:36:26 +01:00
|
|
|
}
|
|
|
|
|
2024-01-10 17:45:41 +01:00
|
|
|
var selectedRange: NSRange {
|
|
|
|
get {
|
|
|
|
guard let textView else {
|
|
|
|
return .init(location: 0, length: 0)
|
|
|
|
}
|
|
|
|
return textView.selectedRange
|
|
|
|
}
|
|
|
|
set {
|
|
|
|
textView?.selectedRange = newValue
|
2024-01-06 03:36:26 +01:00
|
|
|
}
|
|
|
|
}
|
2024-01-10 17:45:41 +01:00
|
|
|
|
|
|
|
var postText: NSMutableAttributedString {
|
2024-01-06 03:36:26 +01:00
|
|
|
didSet {
|
2024-01-10 17:45:41 +01:00
|
|
|
let range = selectedRange
|
|
|
|
formatText()
|
|
|
|
textView?.attributedText = postText
|
|
|
|
selectedRange = range
|
2024-01-06 03:36:26 +01:00
|
|
|
}
|
|
|
|
}
|
2024-01-10 17:45:41 +01:00
|
|
|
var textView: UITextView?
|
|
|
|
|
|
|
|
func append(text: String) {
|
|
|
|
let string = postText
|
|
|
|
string.mutableString.insert(text, at: selectedRange.location)
|
|
|
|
postText = string
|
|
|
|
selectedRange = NSRange(location: selectedRange.location + text.utf16.count, length: 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatText() {
|
|
|
|
postText.addAttributes([.foregroundColor : UIColor.label, .font: UIFont.preferredFont(forTextStyle: .callout), .backgroundColor: UIColor.clear, .underlineColor: UIColor.clear], range: NSMakeRange(0, postText.string.utf16.count))
|
|
|
|
}
|
2024-01-06 03:36:26 +01:00
|
|
|
}
|
2024-01-02 14:23:36 +01:00
|
|
|
}
|