Add places to compose new post.

This commit is contained in:
Marcin Czachursk 2023-02-19 09:41:35 +01:00
parent e229f1e0e1
commit c400091c4f
10 changed files with 269 additions and 22 deletions

View File

@ -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?

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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"
}

View File

@ -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 */,

View File

@ -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)
}
}
}

View File

@ -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() }
}

View File

@ -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)
}
}

View File

@ -60,7 +60,7 @@ struct PhotoEditorView: View {
ActionButton(showLoader: false) {
await self.update()
} label: {
Text("Update")
Text("Save")
}.buttonStyle(.borderedProminent)
}

View File

@ -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)
}
}
}