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-02-19 10:32:38 +01:00
|
|
|
import PixelfedKit
|
2023-01-06 18:16:08 +01:00
|
|
|
|
|
|
|
struct ComposeView: View {
|
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
|
|
|
|
2023-01-31 12:20:49 +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 isSensitive = false
|
|
|
|
@State private var spoilerText = String.empty()
|
|
|
|
@State private var commentsDisabled = false
|
2023-02-19 09:41:35 +01:00
|
|
|
@State private var place: Place?
|
2023-02-18 11:47:49 +01:00
|
|
|
|
2023-02-19 10:16:01 +01:00
|
|
|
@State private var photosAreAttached = 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-19 09:41:35 +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-02-19 09:41:35 +01:00
|
|
|
|
2023-02-21 21:06:03 +01:00
|
|
|
@State private var visibility = Pixelfed.Statuses.Visibility.pub
|
|
|
|
@State private var visibilityText = "Everyone"
|
|
|
|
@State private var visibilityImage = "globe.europe.africa"
|
|
|
|
|
2023-01-10 20:38:02 +01:00
|
|
|
@FocusState private var focusedField: FocusField?
|
2023-02-19 09:41:35 +01:00
|
|
|
enum FocusField: Hashable {
|
|
|
|
case unknown
|
|
|
|
case content
|
|
|
|
case spoilerText
|
|
|
|
}
|
|
|
|
|
|
|
|
@State private var showSheet: SheetType? = nil
|
|
|
|
enum SheetType: Identifiable {
|
|
|
|
case photoDetails(PhotoAttachment)
|
|
|
|
case placeSelector
|
|
|
|
|
|
|
|
public var id: String {
|
|
|
|
switch self {
|
|
|
|
case .photoDetails:
|
|
|
|
return "photoDetails"
|
|
|
|
case .placeSelector:
|
|
|
|
return "placeSelector"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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-19 09:41:35 +01:00
|
|
|
if self.commentsDisabled {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
Text("Comments will be disabled")
|
|
|
|
.textCase(.uppercase)
|
|
|
|
.font(.caption2)
|
|
|
|
.foregroundColor(.dangerColor)
|
|
|
|
}
|
|
|
|
.padding(.horizontal, 8)
|
|
|
|
}
|
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()
|
|
|
|
}
|
2023-02-21 21:06:03 +01:00
|
|
|
.padding(.horizontal, 8)
|
2023-02-17 12:21:09 +01:00
|
|
|
}
|
2023-02-21 21:06:03 +01:00
|
|
|
|
|
|
|
HStack {
|
|
|
|
if let name = self.place?.name, let country = self.place?.country {
|
2023-02-19 09:41:35 +01:00
|
|
|
Group {
|
|
|
|
Image(systemName: "mappin.and.ellipse")
|
|
|
|
Text("\(name), \(country)")
|
|
|
|
}
|
2023-02-18 11:47:49 +01:00
|
|
|
.foregroundColor(.lightGrayColor)
|
2023-02-19 09:41:35 +01:00
|
|
|
}
|
2023-02-21 21:06:03 +01:00
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
Menu {
|
|
|
|
Button {
|
|
|
|
self.visibility = .pub
|
|
|
|
self.visibilityText = "Everyone"
|
|
|
|
self.visibilityImage = "globe.europe.africa"
|
|
|
|
} label: {
|
|
|
|
Label("Everyone", systemImage: "globe.europe.africa")
|
|
|
|
}
|
|
|
|
|
|
|
|
Button {
|
|
|
|
self.visibility = .unlisted
|
|
|
|
self.visibilityText = "Unlisted"
|
|
|
|
self.visibilityImage = "lock.open"
|
|
|
|
} label: {
|
|
|
|
Label("Unlisted", systemImage: "lock.open")
|
|
|
|
}
|
|
|
|
|
|
|
|
Button {
|
|
|
|
self.visibility = .priv
|
|
|
|
self.visibilityText = "Followers"
|
|
|
|
self.visibilityImage = "lock"
|
|
|
|
} label: {
|
|
|
|
Label("Followers", systemImage: "lock")
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
HStack {
|
|
|
|
Label(self.visibilityText, systemImage: self.visibilityImage)
|
|
|
|
Image(systemName: "chevron.down")
|
|
|
|
}
|
|
|
|
.padding(.vertical, 4)
|
|
|
|
.padding(.horizontal, 8)
|
|
|
|
.overlay(
|
|
|
|
RoundedRectangle(cornerRadius: 8)
|
|
|
|
.stroke(Color.accentColor, lineWidth: 1)
|
|
|
|
)
|
|
|
|
}
|
2023-02-18 11:47:49 +01:00
|
|
|
}
|
2023-02-21 21:06:03 +01:00
|
|
|
.font(.footnote)
|
|
|
|
.padding(.horizontal, 8)
|
|
|
|
|
2023-02-18 11:47:49 +01:00
|
|
|
|
2023-02-18 21:06:04 +01:00
|
|
|
TextField("Type what's on your mind", text: $text, axis: .vertical)
|
2023-02-21 21:06:03 +01:00
|
|
|
.padding(.horizontal, 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-19 09:41:35 +01:00
|
|
|
self.showSheet = .photoDetails(photoAttachment)
|
2023-02-18 14:17:18 +01:00
|
|
|
} 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
|
|
|
|
2023-02-19 10:16:01 +01:00
|
|
|
self.photosAreAttached = self.photosAttachment.hasUploadedPhotos()
|
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-19 09:41:35 +01:00
|
|
|
.sheet(item: $showSheet, content: { sheetType in
|
|
|
|
switch sheetType {
|
|
|
|
case .photoDetails(let photoAttachment):
|
|
|
|
PhotoEditorView(photoAttachment: photoAttachment)
|
|
|
|
case .placeSelector:
|
|
|
|
PlaceSelectorView(place: $place)
|
|
|
|
}
|
2023-02-18 14:17:18 +01:00
|
|
|
})
|
2023-02-17 14:47:59 +01:00
|
|
|
.photosPicker(isPresented: $photosPickerVisible, selection: $selectedItems, maxSelectionCount: 4, matching: .images)
|
2023-02-21 08:36:14 +01:00
|
|
|
.navigationTitle("Compose")
|
|
|
|
.navigationBarTitleDisplayMode(.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: {
|
2023-02-19 10:16:01 +01:00
|
|
|
Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle")
|
2023-02-18 11:47:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
2023-02-19 09:41:35 +01:00
|
|
|
|
|
|
|
Button {
|
|
|
|
if self.place != nil {
|
|
|
|
withAnimation(.easeInOut) {
|
|
|
|
self.place = nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.showSheet = .placeSelector
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
Image(systemName: self.place == nil ? "mappin.square" : "mappin.square.fill")
|
|
|
|
}
|
2023-02-18 11:47:49 +01:00
|
|
|
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
}
|
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
|
2023-02-19 10:16:01 +01:00
|
|
|
self.photosAreAttached = self.photosAttachment.hasUploadedPhotos()
|
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
|
|
|
} 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")
|
|
|
|
|
2023-02-09 18:58:54 +01:00
|
|
|
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-02-09 18:58:54 +01:00
|
|
|
}
|
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
|
|
|
|
2023-02-19 10:43:37 +01:00
|
|
|
private func createStatus() -> Pixelfed.Statuses.Components {
|
|
|
|
return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.id,
|
2023-02-17 14:47:59 +01:00
|
|
|
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,
|
2023-02-19 09:41:35 +01:00
|
|
|
placeId: self.place?.id,
|
2023-02-18 11:47:49 +01:00
|
|
|
commentsDisabled: self.commentsDisabled)
|
2023-02-17 14:47:59 +01:00
|
|
|
}
|
2023-01-06 18:16:08 +01:00
|
|
|
}
|