mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-20 11:36:17 +00:00
Editor: Refactor + Add autocomplete for mentions and hashtag
This commit is contained in:
parent
b1c46f2f22
commit
bb47937eb6
9 changed files with 319 additions and 153 deletions
|
@ -57,7 +57,8 @@ public class RouterPath: ObservableObject {
|
||||||
Task {
|
Task {
|
||||||
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString,
|
let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString,
|
||||||
type: "statuses",
|
type: "statuses",
|
||||||
offset: nil),
|
offset: nil,
|
||||||
|
following: nil),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
if let status = results?.statuses.first {
|
if let status = results?.statuses.first {
|
||||||
navigate(to: .statusDetail(id: status.id))
|
navigate(to: .statusDetail(id: status.id))
|
||||||
|
|
|
@ -95,7 +95,8 @@ class ExploreViewModel: ObservableObject {
|
||||||
let apiType = tokens.first?.apiType
|
let apiType = tokens.first?.apiType
|
||||||
var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
|
var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
|
||||||
type: apiType,
|
type: apiType,
|
||||||
offset: nil),
|
offset: nil,
|
||||||
|
following: nil),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
let relationships: [Relationshionship] =
|
let relationships: [Relationshionship] =
|
||||||
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map{ $0.id }))
|
try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map{ $0.id }))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Search: Endpoint {
|
public enum Search: Endpoint {
|
||||||
case search(query: String, type: String?, offset: Int?)
|
case search(query: String, type: String?, offset: Int?, following: Bool?)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -12,7 +12,7 @@ public enum Search: Endpoint {
|
||||||
|
|
||||||
public func queryItems() -> [URLQueryItem]? {
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .search(query, type, offset):
|
case let .search(query, type, offset, following):
|
||||||
var params: [URLQueryItem] = [.init(name: "q", value: query)]
|
var params: [URLQueryItem] = [.init(name: "q", value: query)]
|
||||||
if let type {
|
if let type {
|
||||||
params.append(.init(name: "type", value: type))
|
params.append(.init(name: "type", value: type))
|
||||||
|
@ -20,6 +20,9 @@ public enum Search: Endpoint {
|
||||||
if let offset {
|
if let offset {
|
||||||
params.append(.init(name: "offset", value: String(offset)))
|
params.append(.init(name: "offset", value: String(offset)))
|
||||||
}
|
}
|
||||||
|
if let following {
|
||||||
|
params.append(.init(name: "following", value: following ? "true": "false"))
|
||||||
|
}
|
||||||
params.append(.init(name: "resolve", value: "true"))
|
params.append(.init(name: "resolve", value: "true"))
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import SwiftUI
|
||||||
|
import DesignSystem
|
||||||
|
import PhotosUI
|
||||||
|
import Models
|
||||||
|
import Env
|
||||||
|
|
||||||
|
struct StatusEditorAccessoryView: View {
|
||||||
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
|
|
||||||
|
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
|
||||||
|
@ObservedObject var viewModel: StatusEditorViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Divider()
|
||||||
|
HStack(alignment: .center, spacing: 16) {
|
||||||
|
PhotosPicker(selection: $viewModel.selectedMedias,
|
||||||
|
matching: .images) {
|
||||||
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
viewModel.insertStatusText(text: " @")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "at")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
viewModel.insertStatusText(text: " #")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "number")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
viewModel.spoilerOn.toggle()
|
||||||
|
}
|
||||||
|
isSpoilerTextFocused.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
|
||||||
|
}
|
||||||
|
|
||||||
|
visibilityMenu
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
characterCountView
|
||||||
|
}
|
||||||
|
.frame(height: 20)
|
||||||
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var characterCountView: some View {
|
||||||
|
Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var visibilityMenu: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
|
||||||
|
Button {
|
||||||
|
viewModel.visibility = visibility
|
||||||
|
} label: {
|
||||||
|
Label(visibility.title, systemImage: visibility.iconName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: viewModel.visibility.iconName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import DesignSystem
|
||||||
|
|
||||||
|
struct StatusEditorAutoCompleteView: View {
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
@ObservedObject var viewModel: StatusEditorViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if !viewModel.mentionsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack {
|
||||||
|
if !viewModel.mentionsSuggestions.isEmpty {
|
||||||
|
suggestionsMentionsView
|
||||||
|
} else {
|
||||||
|
suggestionsTagView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
|
}
|
||||||
|
.frame(height: 40)
|
||||||
|
.background(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var suggestionsMentionsView: some View {
|
||||||
|
ForEach(viewModel.mentionsSuggestions) { account in
|
||||||
|
Button {
|
||||||
|
viewModel.selectMentionSuggestion(account: account)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
AvatarView(url: account.avatar, size: .badge)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(account.displayName)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(theme.labelColor)
|
||||||
|
Text("@\(account.acct)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(theme.tintColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var suggestionsTagView: some View {
|
||||||
|
ForEach(viewModel.tagsSuggestions) { tag in
|
||||||
|
Button {
|
||||||
|
viewModel.selectHashtagSuggestion(tag: tag)
|
||||||
|
} label: {
|
||||||
|
Text("#\(tag.name)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(theme.tintColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import DesignSystem
|
||||||
|
import NukeUI
|
||||||
|
|
||||||
|
struct StatusEditorMediaView: View {
|
||||||
|
@ObservedObject var viewModel: StatusEditorViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ForEach(viewModel.mediasImages) { container in
|
||||||
|
if container.image != nil {
|
||||||
|
makeLocalImage(container: container)
|
||||||
|
} else if let url = container.mediaAttachement?.url {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
makeLazyImage(url: url)
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle")
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeLocalImage(container: StatusEditorViewModel.ImageContainer) -> some View {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
Image(uiImage: container.image!)
|
||||||
|
.resizable()
|
||||||
|
.blur(radius: 20 )
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 150, height: 150)
|
||||||
|
.cornerRadius(8)
|
||||||
|
if container.error != nil {
|
||||||
|
VStack {
|
||||||
|
Text("Error uploading")
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
VStack {
|
||||||
|
Text("Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await viewModel.upload(container: container)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
VStack {
|
||||||
|
Text("Retry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeLazyImage(url: URL?) -> some View {
|
||||||
|
LazyImage(url: url) { state in
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizingMode(.aspectFill)
|
||||||
|
.frame(width: 150, height: 150)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.frame(width: 150, height: 150)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 150, height: 150)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,9 +10,7 @@ import NukeUI
|
||||||
|
|
||||||
public struct StatusEditorView: View {
|
public struct StatusEditorView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var quicklook: QuickLook
|
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
|
||||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@ -39,12 +37,17 @@ public struct StatusEditorView: View {
|
||||||
StatusEmbededView(status: status)
|
StatusEmbededView(status: status)
|
||||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
}
|
}
|
||||||
mediasView
|
StatusEditorMediaView(viewModel: viewModel)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
|
.padding(.bottom, 40)
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
StatusEditorAutoCompleteView(viewModel: viewModel)
|
||||||
|
StatusEditorAccessoryView(isSpoilerTextFocused: $isSpoilerTextFocused,
|
||||||
|
viewModel: viewModel)
|
||||||
}
|
}
|
||||||
accessoryView
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
|
@ -116,146 +119,4 @@ public struct StatusEditorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mediasView: some View {
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ForEach(viewModel.mediasImages) { container in
|
|
||||||
if container.image != nil {
|
|
||||||
makeLocalImage(container: container)
|
|
||||||
} else if let url = container.mediaAttachement?.url {
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
makeLazyImage(url: url)
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "xmark.circle")
|
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeLocalImage(container: StatusEditorViewModel.ImageContainer) -> some View {
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
Image(uiImage: container.image!)
|
|
||||||
.resizable()
|
|
||||||
.blur(radius: 20 )
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(width: 150, height: 150)
|
|
||||||
.cornerRadius(8)
|
|
||||||
if container.error != nil {
|
|
||||||
VStack {
|
|
||||||
Text("Error uploading")
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
VStack {
|
|
||||||
Text("Delete")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
await viewModel.upload(container: container)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
VStack {
|
|
||||||
Text("Retry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeLazyImage(url: URL?) -> some View {
|
|
||||||
LazyImage(url: url) { state in
|
|
||||||
if let image = state.image {
|
|
||||||
image
|
|
||||||
.resizingMode(.aspectFill)
|
|
||||||
.frame(width: 150, height: 150)
|
|
||||||
} else {
|
|
||||||
Rectangle()
|
|
||||||
.frame(width: 150, height: 150)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 150, height: 150)
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var accessoryView: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
Divider()
|
|
||||||
HStack(alignment: .center, spacing: 16) {
|
|
||||||
PhotosPicker(selection: $viewModel.selectedMedias,
|
|
||||||
matching: .images) {
|
|
||||||
Image(systemName: "photo.fill.on.rectangle.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.insertStatusText(text: " @")
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "at")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.insertStatusText(text: " #")
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "number")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
viewModel.spoilerOn.toggle()
|
|
||||||
}
|
|
||||||
isSpoilerTextFocused.toggle()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
|
|
||||||
}
|
|
||||||
|
|
||||||
visibilityMenu
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
characterCountView
|
|
||||||
}
|
|
||||||
.frame(height: 20)
|
|
||||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.background(.ultraThinMaterial)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var characterCountView: some View {
|
|
||||||
Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var visibilityMenu: some View {
|
|
||||||
Menu {
|
|
||||||
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
|
|
||||||
Button {
|
|
||||||
viewModel.visibility = visibility
|
|
||||||
} label: {
|
|
||||||
Label(visibility.title, systemImage: visibility.iconName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: viewModel.visibility.iconName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
|
|
||||||
@Published var visibility: Models.Visibility = .pub
|
@Published var visibility: Models.Visibility = .pub
|
||||||
|
|
||||||
|
@Published var mentionsSuggestions: [Account] = []
|
||||||
|
@Published var tagsSuggestions: [Tag] = []
|
||||||
|
private var currentSuggestionRange: NSRange?
|
||||||
|
|
||||||
private var embededStatusURL: URL? {
|
private var embededStatusURL: URL? {
|
||||||
return embededStatus?.reblog?.url ?? embededStatus?.url
|
return embededStatus?.reblog?.url ?? embededStatus?.url
|
||||||
}
|
}
|
||||||
|
@ -63,6 +67,14 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
selectedRange = NSRange(location: selectedRange.location + text.utf16.count, length: 0)
|
selectedRange = NSRange(location: selectedRange.location + text.utf16.count, length: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func replaceTextWith(text: String, inRange: NSRange) {
|
||||||
|
let string = statusText
|
||||||
|
string.mutableString.deleteCharacters(in: inRange)
|
||||||
|
string.mutableString.insert(text, at: inRange.location)
|
||||||
|
statusText = string
|
||||||
|
selectedRange = NSRange(location: inRange.location + text.utf16.count, length: 0)
|
||||||
|
}
|
||||||
|
|
||||||
func postStatus() async -> Status? {
|
func postStatus() async -> Status? {
|
||||||
guard let client else { return nil }
|
guard let client else { return nil }
|
||||||
do {
|
do {
|
||||||
|
@ -143,9 +155,20 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
options: [],
|
options: [],
|
||||||
range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range }
|
range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range }
|
||||||
|
|
||||||
for range in ranges {
|
var foundSuggestionRange: Bool = false
|
||||||
|
for nsRange in ranges {
|
||||||
statusText.addAttributes([.foregroundColor: UIColor(Color.brand)],
|
statusText.addAttributes([.foregroundColor: UIColor(Color.brand)],
|
||||||
range: NSRange(location: range.location, length: range.length))
|
range: nsRange)
|
||||||
|
if selectedRange.location == (nsRange.location + nsRange.length),
|
||||||
|
let range = Range(nsRange, in: statusText.string) {
|
||||||
|
foundSuggestionRange = true
|
||||||
|
currentSuggestionRange = nsRange
|
||||||
|
loadAutoCompleteResults(query: String(statusText.string[range]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundSuggestionRange || ranges.isEmpty{
|
||||||
|
resetAutoCompletion()
|
||||||
}
|
}
|
||||||
|
|
||||||
for range in urlRanges {
|
for range in urlRanges {
|
||||||
|
@ -167,6 +190,60 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Autocomplete
|
||||||
|
|
||||||
|
private func loadAutoCompleteResults(query: String) {
|
||||||
|
guard let client, query.utf8.count > 1 else { return }
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
var results: SearchResults?
|
||||||
|
switch query.first {
|
||||||
|
case "#":
|
||||||
|
results = try await client.get(endpoint: Search.search(query: query,
|
||||||
|
type: "hashtags",
|
||||||
|
offset: 0,
|
||||||
|
following: nil),
|
||||||
|
forceVersion: .v2)
|
||||||
|
withAnimation {
|
||||||
|
tagsSuggestions = results?.hashtags ?? []
|
||||||
|
}
|
||||||
|
case "@":
|
||||||
|
results = try await client.get(endpoint: Search.search(query: query,
|
||||||
|
type: "accounts",
|
||||||
|
offset: 0,
|
||||||
|
following: true),
|
||||||
|
forceVersion: .v2)
|
||||||
|
withAnimation {
|
||||||
|
mentionsSuggestions = results?.accounts ?? []
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetAutoCompletion() {
|
||||||
|
tagsSuggestions = []
|
||||||
|
mentionsSuggestions = []
|
||||||
|
currentSuggestionRange = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectMentionSuggestion(account: Account) {
|
||||||
|
if let range = currentSuggestionRange {
|
||||||
|
replaceTextWith(text: "@\(account.acct) ", inRange: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectHashtagSuggestion(tag: Tag) {
|
||||||
|
if let range = currentSuggestionRange {
|
||||||
|
replaceTextWith(text: "#\(tag.name) ", inRange: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Media related function
|
// MARK: - Media related function
|
||||||
|
|
||||||
private func indexOf(container: ImageContainer) -> Int? {
|
private func indexOf(container: ImageContainer) -> Int? {
|
||||||
|
|
|
@ -56,7 +56,8 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
} else {
|
} else {
|
||||||
let results: SearchResults = try await client.get(endpoint: Search.search(query: url.absoluteString,
|
let results: SearchResults = try await client.get(endpoint: Search.search(query: url.absoluteString,
|
||||||
type: "statuses",
|
type: "statuses",
|
||||||
offset: 0),
|
offset: 0,
|
||||||
|
following: nil),
|
||||||
forceVersion: .v2)
|
forceVersion: .v2)
|
||||||
embed = results.statuses.first
|
embed = results.statuses.first
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue