Add places to compose new post.
This commit is contained in:
parent
e229f1e0e1
commit
c400091c4f
|
@ -11,7 +11,7 @@ import Foundation
|
|||
public struct Place: Codable {
|
||||
|
||||
/// Id of the entity.
|
||||
public let id: Int32
|
||||
public let id: Int
|
||||
|
||||
/// City where picture has been taken.
|
||||
public let slug: String?
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension MastodonClientAuthenticated {
|
||||
|
||||
func places(query: String) async throws -> [Place] {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Places.search(query),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
return try await downloadJson([Place].self, request: request)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon {
|
||||
public enum Places {
|
||||
case search(SearchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Places: TargetType {
|
||||
fileprivate var apiPath: String { return "/api/v1.1/compose/search/location" }
|
||||
|
||||
/// The path to be appended to `baseURL` to form the full `URL`.
|
||||
public var path: String {
|
||||
switch self {
|
||||
case .search:
|
||||
return "\(apiPath)"
|
||||
}
|
||||
}
|
||||
|
||||
/// The HTTP method used in the request.
|
||||
public var method: Method {
|
||||
switch self {
|
||||
case .search:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters to be incoded in the request.
|
||||
public var queryItems: [(String, String)]? {
|
||||
switch self {
|
||||
case .search(let query):
|
||||
return [
|
||||
("q", query)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
public var headers: [String: String]? {
|
||||
[:].contentTypeApplicationJson
|
||||
}
|
||||
|
||||
public var httpBody: Data? {
|
||||
nil
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
extension Mastodon {
|
||||
public enum Statuses {
|
||||
public enum Visibility: String, Encodable {
|
||||
case direct = "direct"
|
||||
case priv = "private"
|
||||
case unlisted = "unlisted"
|
||||
case pub = "public"
|
||||
}
|
||||
|
|
|
@ -114,6 +114,8 @@
|
|||
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DB296EAACE0062125F /* SettingsView.swift */; };
|
||||
F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89A46DD296EABA20062125F /* StatusPlaceholder.swift */; };
|
||||
F89AC00529A1F9B500F4159F /* Servers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00429A1F9B500F4159F /* Servers.swift */; };
|
||||
F89AC00729A208CC00F4159F /* PlaceSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00629A208CC00F4159F /* PlaceSelectorView.swift */; };
|
||||
F89AC00929A20C5C00F4159F /* Client+Places.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89AC00829A20C5C00F4159F /* Client+Places.swift */; };
|
||||
F89CEB802984198600A1376F /* AttachmentData+HighestImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */; };
|
||||
F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C3E29716E41001DA3D4 /* Theme.swift */; };
|
||||
F89D6C4229717FDC001DA3D4 /* AccountsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4129717FDC001DA3D4 /* AccountsSection.swift */; };
|
||||
|
@ -252,6 +254,8 @@
|
|||
F89A46DB296EAACE0062125F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
F89A46DD296EABA20062125F /* StatusPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPlaceholder.swift; sourceTree = "<group>"; };
|
||||
F89AC00429A1F9B500F4159F /* Servers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Servers.swift; sourceTree = "<group>"; };
|
||||
F89AC00629A208CC00F4159F /* PlaceSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceSelectorView.swift; sourceTree = "<group>"; };
|
||||
F89AC00829A20C5C00F4159F /* Client+Places.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Client+Places.swift"; sourceTree = "<group>"; };
|
||||
F89CEB7F2984198600A1376F /* AttachmentData+HighestImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+HighestImage.swift"; sourceTree = "<group>"; };
|
||||
F89D6C3E29716E41001DA3D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
F89D6C4129717FDC001DA3D4 /* AccountsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSection.swift; sourceTree = "<group>"; };
|
||||
|
@ -359,6 +363,7 @@
|
|||
F88E4D47297E90CD0057491A /* TrendStatusesView.swift */,
|
||||
F8B0885D29942E31002AB40A /* ThirdPartyView.swift */,
|
||||
F8FA991D299FAB92007AB130 /* PhotoEditorView.swift */,
|
||||
F89AC00629A208CC00F4159F /* PlaceSelectorView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -608,6 +613,7 @@
|
|||
F8FA9916299F7DBD007AB130 /* Client+Media.swift */,
|
||||
F8B9B350298D4B34009CC69C /* Client+Account.swift */,
|
||||
F8B9B352298D4B5D009CC69C /* Client+Search.swift */,
|
||||
F89AC00829A20C5C00F4159F /* Client+Places.swift */,
|
||||
F8B9B355298D4C1E009CC69C /* Client+Instance.swift */,
|
||||
F86A4302299A9AF500DF7645 /* TipsStore.swift */,
|
||||
);
|
||||
|
@ -782,6 +788,7 @@
|
|||
F89D6C4A297196FF001DA3D4 /* ImagesViewer.swift in Sources */,
|
||||
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
|
||||
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
||||
F89AC00729A208CC00F4159F /* PlaceSelectorView.swift in Sources */,
|
||||
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */,
|
||||
F8B9B349298D4AA2009CC69C /* Client+Timeline.swift in Sources */,
|
||||
F8FA9919299FA35A007AB130 /* PhotoAttachment.swift in Sources */,
|
||||
|
@ -797,6 +804,7 @@
|
|||
F89D6C4629718193001DA3D4 /* ThemeSection.swift in Sources */,
|
||||
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
|
||||
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
|
||||
F89AC00929A20C5C00F4159F /* Client+Places.swift in Sources */,
|
||||
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */,
|
||||
F857F9FD297D8ED3002C109C /* ActionMenu.swift in Sources */,
|
||||
F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
/// Mastodon 'Places'.
|
||||
extension Client {
|
||||
public class Places: BaseClient {
|
||||
public func search(query: String) async throws -> [Place] {
|
||||
return try await mastodonClient.places(query: query)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ extension Client {
|
|||
public var media: Media? { return Media(mastodonClient: self.mastodonClient) }
|
||||
public var accounts: Accounts? { return Accounts(mastodonClient: self.mastodonClient) }
|
||||
public var search: Search? { return Search(mastodonClient: self.mastodonClient) }
|
||||
public var places: Places? { return Places(mastodonClient: self.mastodonClient) }
|
||||
public var instances: Instances { return Instances() }
|
||||
}
|
||||
|
||||
|
|
|
@ -9,12 +9,6 @@ import PhotosUI
|
|||
import MastodonKit
|
||||
|
||||
struct ComposeView: View {
|
||||
enum FocusField: Hashable {
|
||||
case unknown
|
||||
case content
|
||||
case spoilerText
|
||||
}
|
||||
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
@EnvironmentObject var client: Client
|
||||
|
@ -27,18 +21,38 @@ struct ComposeView: View {
|
|||
@State private var isSensitive = false
|
||||
@State private var spoilerText = String.empty()
|
||||
@State private var commentsDisabled = false
|
||||
@State private var place: Place?
|
||||
|
||||
@State private var publishDisabled = true
|
||||
@State private var interactiveDismissDisabled = false
|
||||
|
||||
@State private var photosAreUploading = false
|
||||
@State private var photosPickerVisible = false
|
||||
@State private var showPhoto: PhotoAttachment? = nil
|
||||
|
||||
|
||||
@State private var selectedItems: [PhotosPickerItem] = []
|
||||
@State private var photosAttachment: [PhotoAttachment] = []
|
||||
|
||||
|
||||
@FocusState private var focusedField: FocusField?
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
|
||||
|
||||
|
@ -56,6 +70,16 @@ struct ComposeView: View {
|
|||
.background(Color.dangerColor.opacity(0.4))
|
||||
}
|
||||
|
||||
if self.commentsDisabled {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Comments will be disabled")
|
||||
.textCase(.uppercase)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.dangerColor)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
|
||||
if let accountData = applicationState.account {
|
||||
HStack {
|
||||
|
@ -69,12 +93,17 @@ struct ComposeView: View {
|
|||
.padding(8)
|
||||
}
|
||||
|
||||
if self.commentsDisabled {
|
||||
Text("Comments will be disabled")
|
||||
.textCase(.uppercase)
|
||||
.font(.caption2)
|
||||
.padding(.horizontal, 8)
|
||||
if let name = self.place?.name, let country = self.place?.country {
|
||||
HStack {
|
||||
Group {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
Text("\(name), \(country)")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
|
||||
TextField("Type what's on your mind", text: $text, axis: .vertical)
|
||||
|
@ -96,7 +125,7 @@ struct ComposeView: View {
|
|||
HStack(alignment: .center) {
|
||||
ForEach(self.photosAttachment, id: \.id) { photoAttachment in
|
||||
ImageUploadView(photoAttachment: photoAttachment) {
|
||||
self.showPhoto = photoAttachment
|
||||
self.showSheet = .photoDetails(photoAttachment)
|
||||
} delete: {
|
||||
self.photosAttachment = self.photosAttachment.filter({ item in
|
||||
item != photoAttachment
|
||||
|
@ -160,8 +189,13 @@ struct ComposeView: View {
|
|||
await self.loadPhotos()
|
||||
}
|
||||
}
|
||||
.sheet(item: $showPhoto, content: { item in
|
||||
PhotoEditorView(photoAttachment: item)
|
||||
.sheet(item: $showSheet, content: { sheetType in
|
||||
switch sheetType {
|
||||
case .photoDetails(let photoAttachment):
|
||||
PhotoEditorView(photoAttachment: photoAttachment)
|
||||
case .placeSelector:
|
||||
PlaceSelectorView(place: $place)
|
||||
}
|
||||
})
|
||||
.photosPicker(isPresented: $photosPickerVisible, selection: $selectedItems, maxSelectionCount: 4, matching: .images)
|
||||
.navigationBarTitle(Text("Compose"), displayMode: .inline)
|
||||
|
@ -205,6 +239,18 @@ struct ComposeView: View {
|
|||
} label: {
|
||||
Image(systemName: self.commentsDisabled ? "person.2.slash" : "person.2.fill")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
|
@ -222,7 +268,7 @@ struct ComposeView: View {
|
|||
HStack {
|
||||
Image(systemName: "lock")
|
||||
Text(" Followers")
|
||||
}.tag(Mastodon.Statuses.Visibility.direct)
|
||||
}.tag(Mastodon.Statuses.Visibility.priv)
|
||||
}.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
@ -320,13 +366,13 @@ struct ComposeView: View {
|
|||
}
|
||||
|
||||
private func createStatus() -> Mastodon.Statuses.Components {
|
||||
// TODO: Missing fields: placeId, collectionIds.
|
||||
return Mastodon.Statuses.Components(inReplyToId: self.statusViewModel?.id,
|
||||
text: self.text,
|
||||
spoilerText: self.isSensitive ? self.spoilerText : String.empty(),
|
||||
mediaIds: self.photosAttachment.getUploadedPhotoIds(),
|
||||
visibility: self.visibility,
|
||||
sensitive: self.isSensitive,
|
||||
placeId: self.place?.id,
|
||||
commentsDisabled: self.commentsDisabled)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ struct PhotoEditorView: View {
|
|||
ActionButton(showLoader: false) {
|
||||
await self.update()
|
||||
} label: {
|
||||
Text("Update")
|
||||
Text("Save")
|
||||
}.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MastodonKit
|
||||
|
||||
struct PlaceSelectorView: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var client: Client
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var places: [Place] = []
|
||||
@State private var query = String.empty()
|
||||
|
||||
@Binding public var place: Place?
|
||||
|
||||
@FocusState private var focusedField: FocusField?
|
||||
enum FocusField: Hashable {
|
||||
case unknown
|
||||
case search
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack(alignment: .leading) {
|
||||
List {
|
||||
Section {
|
||||
HStack {
|
||||
TextField("Search...", text: $query)
|
||||
.padding(8)
|
||||
.focused($focusedField, equals: .search)
|
||||
.keyboardType(.default)
|
||||
.autocorrectionDisabled()
|
||||
.onAppear() {
|
||||
self.focusedField = .search
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
await self.searchPlaces()
|
||||
}
|
||||
} label: {
|
||||
Text("Search")
|
||||
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(self.places, id: \.id) { place in
|
||||
Button {
|
||||
self.place = place
|
||||
self.dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(place.name ?? String.empty())
|
||||
.foregroundColor(.mainTextColor)
|
||||
Text(place.country ?? String.empty())
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
if self.place?.id == place.id {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(self.applicationState.tintColor.color())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(Text("Places"), displayMode: .inline)
|
||||
.toolbar {
|
||||
self.getTrailingToolbar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private func getTrailingToolbar() -> some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func searchPlaces() async {
|
||||
do {
|
||||
if let placesFromApi = try await self.client.places?.search(query: self.query) {
|
||||
self.places = placesFromApi
|
||||
}
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Cannot download places.", showToastr: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue