Impressia/Vernissage/Views/ComposeView.swift

333 lines
14 KiB
Swift
Raw Normal View History

2023-01-06 18:16:08 +01:00
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
2023-02-14 18:40:08 +01:00
import PhotosUI
2023-01-10 11:30:30 +01:00
import MastodonKit
2023-01-06 18:16:08 +01:00
struct ComposeView: View {
2023-01-10 20:38:02 +01:00
enum FocusField: Hashable {
2023-02-14 18:40:08 +01:00
case unknown
2023-01-10 20:38:02 +01:00
case content
2023-02-18 11:47:49 +01:00
case spoilerText
2023-01-10 20:38:02 +01:00
}
2023-02-18 11:47:49 +01:00
2023-01-10 11:30:30 +01:00
@EnvironmentObject var applicationState: ApplicationState
2023-02-17 14:47:59 +01:00
@EnvironmentObject var routerPath: RouterPath
2023-02-03 15:16:30 +01:00
@EnvironmentObject var client: Client
2023-02-17 14:47:59 +01:00
2023-01-06 18:16:08 +01:00
@Environment(\.dismiss) private var dismiss
2023-01-10 11:30:30 +01:00
@State var statusViewModel: StatusModel?
2023-01-14 08:52:51 +01:00
@State private var text = String.empty()
2023-02-18 11:47:49 +01:00
@State private var visibility = Mastodon.Statuses.Visibility.pub
@State private var isSensitive = false
@State private var spoilerText = String.empty()
@State private var commentsDisabled = false
2023-02-17 12:21:09 +01:00
@State private var publishDisabled = true
2023-02-18 11:47:49 +01:00
@State private var interactiveDismissDisabled = false
2023-02-14 18:40:08 +01:00
2023-02-17 12:21:09 +01:00
@State private var photosAreUploading = false
2023-02-14 18:40:08 +01:00
@State private var photosPickerVisible = false
2023-02-18 14:17:18 +01:00
@State private var showPhoto: PhotoAttachment? = nil
2023-02-17 14:47:59 +01:00
2023-02-14 18:40:08 +01:00
@State private var selectedItems: [PhotosPickerItem] = []
2023-02-17 14:47:59 +01:00
@State private var photosAttachment: [PhotoAttachment] = []
2023-01-10 20:38:02 +01:00
@FocusState private var focusedField: FocusField?
2023-01-10 11:30:30 +01:00
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
2023-01-06 18:16:08 +01:00
var body: some View {
2023-02-17 14:47:59 +01:00
NavigationStack {
NavigationView {
ScrollView {
VStack (alignment: .leading){
2023-02-18 11:47:49 +01:00
if self.isSensitive {
2023-02-19 07:50:21 +01:00
TextField("Write content warning", text: $spoilerText, axis: .vertical)
2023-02-18 11:47:49 +01:00
.padding(8)
2023-02-19 07:50:21 +01:00
.lineLimit(1...2)
2023-02-18 11:47:49 +01:00
.focused($focusedField, equals: .spoilerText)
.keyboardType(.default)
2023-02-19 07:50:21 +01:00
.background(Color.dangerColor.opacity(0.4))
2023-02-18 11:47:49 +01:00
}
2023-02-17 14:47:59 +01:00
if let accountData = applicationState.account {
HStack {
UsernameRow(
accountId: accountData.id,
accountAvatar: accountData.avatar,
accountDisplayName: accountData.displayName,
accountUsername: accountData.username)
Spacer()
}
.padding(8)
2023-02-17 12:21:09 +01:00
}
2023-02-17 14:47:59 +01:00
2023-02-18 11:47:49 +01:00
if self.commentsDisabled {
Text("Comments will be disabled")
.textCase(.uppercase)
.font(.caption2)
.padding(.horizontal, 8)
.foregroundColor(.lightGrayColor)
}
2023-02-18 21:06:04 +01:00
TextField("Type what's on your mind", text: $text, axis: .vertical)
2023-02-17 14:47:59 +01:00
.padding(8)
2023-02-18 21:06:04 +01:00
.lineLimit(2...12)
2023-02-17 14:47:59 +01:00
.focused($focusedField, equals: .content)
2023-02-18 11:47:49 +01:00
.keyboardType(.default)
.onFirstAppear {
2023-02-17 14:47:59 +01:00
self.focusedField = .content
}
.onChange(of: self.text) { newValue in
self.publishDisabled = self.isPublishButtonDisabled()
2023-02-18 11:47:49 +01:00
self.interactiveDismissDisabled = self.isInteractiveDismissDisabled()
2023-02-17 14:47:59 +01:00
}
.toolbar {
2023-02-18 11:47:49 +01:00
self.keyboardToolbar()
2023-02-14 18:40:08 +01:00
}
2023-02-17 14:47:59 +01:00
HStack(alignment: .center) {
ForEach(self.photosAttachment, id: \.id) { photoAttachment in
2023-02-17 17:10:09 +01:00
ImageUploadView(photoAttachment: photoAttachment) {
2023-02-18 14:17:18 +01:00
self.showPhoto = photoAttachment
} delete: {
2023-02-17 17:10:09 +01:00
self.photosAttachment = self.photosAttachment.filter({ item in
item != photoAttachment
})
2023-02-18 14:17:18 +01:00
self.publishDisabled = self.isPublishButtonDisabled()
self.interactiveDismissDisabled = self.isInteractiveDismissDisabled()
2023-02-17 17:10:09 +01:00
}
2023-02-14 18:40:08 +01:00
}
}
2023-02-17 14:47:59 +01:00
.padding(8)
if let status = self.statusViewModel {
HStack (alignment: .top) {
UserAvatar(accountAvatar: status.account.avatar, size: .comment)
VStack (alignment: .leading, spacing: 0) {
HStack (alignment: .top) {
Text(statusViewModel?.account.displayNameWithoutEmojis ?? "")
.foregroundColor(.mainTextColor)
.font(.footnote)
.fontWeight(.bold)
Spacer()
}
MarkdownFormattedText(status.content.asMarkdown, withFontSize: 14, andWidth: contentWidth)
.environment(\.openURL, OpenURLAction { url in .handled })
2023-01-10 11:30:30 +01:00
}
}
2023-02-17 14:47:59 +01:00
.padding(8)
.background(Color.selectedRowColor)
2023-01-10 11:30:30 +01:00
}
2023-02-17 14:47:59 +01:00
Spacer()
2023-01-10 11:30:30 +01:00
}
}
2023-02-18 11:47:49 +01:00
.onTapGesture {
self.hideKeyboard()
}
2023-02-17 14:47:59 +01:00
.frame(alignment: .topLeading)
.toolbar {
ToolbarItem(placement: .primaryAction) {
2023-02-18 14:17:18 +01:00
ActionButton(showLoader: false) {
2023-02-18 13:43:22 +01:00
await self.publishStatus()
2023-02-17 14:47:59 +01:00
} label: {
Text("Publish")
}
.disabled(self.publishDisabled)
.buttonStyle(.borderedProminent)
}
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", role: .cancel) {
2023-01-10 20:38:02 +01:00
dismiss()
}
2023-01-06 18:16:08 +01:00
}
}
2023-02-17 14:47:59 +01:00
.onChange(of: self.selectedItems) { selectedItem in
Task {
await self.loadPhotos()
2023-01-06 18:16:08 +01:00
}
}
2023-02-18 14:17:18 +01:00
.sheet(item: $showPhoto, content: { item in
PhotoEditorView(photoAttachment: item)
})
2023-02-17 14:47:59 +01:00
.photosPicker(isPresented: $photosPickerVisible, selection: $selectedItems, maxSelectionCount: 4, matching: .images)
.navigationBarTitle(Text("Compose"), displayMode: .inline)
2023-01-06 18:16:08 +01:00
}
2023-02-17 14:47:59 +01:00
.withAppRouteur()
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
2023-01-06 18:16:08 +01:00
}
2023-02-18 11:47:49 +01:00
.interactiveDismissDisabled(self.interactiveDismissDisabled)
}
@ToolbarContentBuilder
private func keyboardToolbar() -> some ToolbarContent {
ToolbarItemGroup(placement: .keyboard) {
HStack(alignment: .center) {
Button {
hideKeyboard()
self.focusedField = .unknown
self.photosPickerVisible = true
} label: {
Image(systemName: "photo.on.rectangle.angled")
}
Button {
withAnimation(.easeInOut) {
self.isSensitive.toggle()
2023-02-18 21:06:04 +01:00
if self.isSensitive {
self.focusedField = .spoilerText
} else {
self.focusedField = .content
}
2023-02-18 11:47:49 +01:00
}
} label: {
Image(systemName: self.isSensitive ? "exclamationmark.square.fill" : "exclamationmark.square")
}
Button {
withAnimation(.easeInOut) {
self.commentsDisabled.toggle()
}
} label: {
Image(systemName: self.commentsDisabled ? "person.2.slash" : "person.2.fill")
}
Spacer()
Picker("Post visibility", selection: $visibility) {
HStack {
Image(systemName: "globe.europe.africa")
Text(" Everyone")
}.tag(Mastodon.Statuses.Visibility.pub)
HStack {
Image(systemName: "lock.open")
Text(" Unlisted")
}.tag(Mastodon.Statuses.Visibility.unlisted)
HStack {
Image(systemName: "lock")
Text(" Followers")
}.tag(Mastodon.Statuses.Visibility.direct)
}.buttonStyle(.bordered)
}
}
2023-01-06 18:16:08 +01:00
}
2023-01-10 11:30:30 +01:00
2023-02-17 12:21:09 +01:00
private func isPublishButtonDisabled() -> Bool {
// Publish always disabled when there is not status text.
if self.text.isEmpty {
return true
}
// When application is during uploading photos we cannot send new status.
if self.photosAreUploading == true {
return true
}
// When status is not a comment, then photo is required.
2023-02-17 14:47:59 +01:00
if self.statusViewModel == nil && self.photosAttachment.hasUploadedPhotos() == false {
2023-02-17 12:21:09 +01:00
return true
}
return false
}
2023-02-18 11:47:49 +01:00
private func isInteractiveDismissDisabled() -> Bool {
if self.text.isEmpty == false {
return true
}
if self.photosAreUploading == true {
return true
}
if self.photosAttachment.hasUploadedPhotos() == true {
return true
}
return false
}
2023-02-17 12:21:09 +01:00
private func loadPhotos() async {
do {
self.photosAreUploading = true
2023-02-17 14:47:59 +01:00
self.photosAttachment = []
2023-02-17 12:21:09 +01:00
self.publishDisabled = self.isPublishButtonDisabled()
2023-02-18 11:47:49 +01:00
self.interactiveDismissDisabled = self.isInteractiveDismissDisabled()
2023-02-17 12:21:09 +01:00
for item in self.selectedItems {
2023-02-17 14:47:59 +01:00
if let photoData = try await item.loadTransferable(type: Data.self) {
self.photosAttachment.append(PhotoAttachment(photosPickerItem: item, photoData: photoData))
2023-02-17 12:21:09 +01:00
}
}
self.focusedField = .content
await self.upload()
self.photosAreUploading = false
self.publishDisabled = self.isPublishButtonDisabled()
2023-02-18 11:47:49 +01:00
self.interactiveDismissDisabled = self.isInteractiveDismissDisabled()
2023-02-17 12:21:09 +01:00
} catch {
ErrorService.shared.handle(error, message: "Cannot retreive image from library.", showToastr: true)
}
}
private func upload() async {
2023-02-17 14:47:59 +01:00
for (index, photoAttachment) in self.photosAttachment.enumerated() {
2023-02-17 12:21:09 +01:00
do {
2023-02-17 14:47:59 +01:00
if let mediaAttachment = try await self.client.media?.upload(data: photoAttachment.photoData,
2023-02-17 12:21:09 +01:00
fileName: "file-\(index).jpg",
2023-02-17 14:47:59 +01:00
mimeType: "image/jpeg") {
photoAttachment.uploadedAttachment = mediaAttachment
2023-02-17 12:21:09 +01:00
}
} catch {
2023-02-17 14:47:59 +01:00
photoAttachment.error = error
2023-02-17 12:21:09 +01:00
ErrorService.shared.handle(error, message: "Error during post photo.", showToastr: true)
}
}
}
2023-01-10 20:38:02 +01:00
private func publishStatus() async {
do {
2023-02-17 14:47:59 +01:00
let status = self.createStatus()
if let newStatus = try await self.client.statuses?.new(status: status) {
2023-02-17 12:21:09 +01:00
ToastrService.shared.showSuccess("Status published", imageSystemName: "message.fill")
let statusModel = StatusModel(status: newStatus)
let commentModel = CommentModel(status: statusModel, showDivider: false)
self.applicationState.newComment = commentModel
2023-02-18 13:43:22 +01:00
dismiss()
}
2023-01-10 20:38:02 +01:00
} catch {
2023-01-15 12:41:55 +01:00
ErrorService.shared.handle(error, message: "Error during post status.", showToastr: true)
2023-01-10 20:38:02 +01:00
}
}
2023-02-17 14:47:59 +01:00
private func createStatus() -> Mastodon.Statuses.Components {
2023-02-18 11:47:49 +01:00
// TODO: Missing fields: placeId, collectionIds.
2023-02-17 14:47:59 +01:00
return Mastodon.Statuses.Components(inReplyToId: self.statusViewModel?.id,
text: self.text,
2023-02-18 11:47:49 +01:00
spoilerText: self.isSensitive ? self.spoilerText : String.empty(),
mediaIds: self.photosAttachment.getUploadedPhotoIds(),
visibility: self.visibility,
sensitive: self.isSensitive,
commentsDisabled: self.commentsDisabled)
2023-02-17 14:47:59 +01:00
}
2023-01-06 18:16:08 +01:00
}