Add support for custom emojis in the composer close #98
This commit is contained in:
parent
fd6f337571
commit
9c532d9448
|
@ -0,0 +1,16 @@
|
|||
import Foundation
|
||||
|
||||
public enum CustomEmojis: Endpoint {
|
||||
case customEmojis
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
case .customEmojis:
|
||||
return "custom_emojis"
|
||||
}
|
||||
}
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
nil
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import Env
|
|||
import Models
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct StatusEditorAccessoryView: View {
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
|
@ -14,6 +15,7 @@ struct StatusEditorAccessoryView: View {
|
|||
|
||||
@State private var isDraftsSheetDisplayed: Bool = false
|
||||
@State private var isLanguageSheetDisplayed: Bool = false
|
||||
@State private var isCustomEmojisSheetDisplay: Bool = false
|
||||
@State private var languageSearch: String = ""
|
||||
|
||||
var body: some View {
|
||||
|
@ -52,6 +54,14 @@ struct StatusEditorAccessoryView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if !viewModel.customEmojis.isEmpty {
|
||||
Button {
|
||||
isCustomEmojisSheetDisplay = true
|
||||
} label: {
|
||||
Image(systemName: "face.smiling.inverse")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
isLanguageSheetDisplayed.toggle()
|
||||
} label: {
|
||||
|
@ -74,9 +84,12 @@ struct StatusEditorAccessoryView: View {
|
|||
.sheet(isPresented: $isDraftsSheetDisplayed) {
|
||||
draftsSheetView
|
||||
}
|
||||
.sheet(isPresented: $isLanguageSheetDisplayed, content: {
|
||||
.sheet(isPresented: $isLanguageSheetDisplayed) {
|
||||
languageSheetView
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $isCustomEmojisSheetDisplay) {
|
||||
customEmojisSheet
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.setInitialLanguageSelection(preference: preferences.serverPreferences?.postLanguage)
|
||||
}
|
||||
|
@ -155,8 +168,40 @@ struct StatusEditorAccessoryView: View {
|
|||
.presentationDetents([.medium])
|
||||
}
|
||||
|
||||
private var customEmojisSheet: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 40))], spacing: 9) {
|
||||
ForEach(viewModel.customEmojis) { emoji in
|
||||
LazyImage(url: emoji.url) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizingMode(.aspectFit)
|
||||
.frame(width: 40, height: 40)
|
||||
} else if state.isLoading {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
.frame(width: 40, height: 40)
|
||||
.shimmering()
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
viewModel.insertStatusText(text: " :\(emoji.shortcode): ")
|
||||
isCustomEmojisSheetDisplay = false
|
||||
}
|
||||
}
|
||||
}.padding(.horizontal)
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.navigationTitle("Custom Emojis")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
}
|
||||
|
||||
private var characterCountView: some View {
|
||||
Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
|
||||
Text("\((currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
|
||||
.foregroundColor(.gray)
|
||||
.font(.scaledCallout)
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ struct StatusEditorPollView: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: viewModel.pollOptions[index]) {
|
||||
let maxCharacters: Int = currentInstance.instance?.configuration.polls.maxCharactersPerOption ?? 50
|
||||
let maxCharacters: Int = currentInstance.instance?.configuration?.polls.maxCharactersPerOption ?? 50
|
||||
viewModel.pollOptions[index] = String($0.prefix(maxCharacters))
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ struct StatusEditorPollView: View {
|
|||
|
||||
private func canAddMoreAt(_ index: Int) -> Bool {
|
||||
let count = viewModel.pollOptions.count
|
||||
let maxEntries: Int = currentInstance.instance?.configuration.polls.maxOptions ?? 4
|
||||
let maxEntries: Int = currentInstance.instance?.configuration?.polls.maxOptions ?? 4
|
||||
|
||||
return index == count - 1 && count < maxEntries
|
||||
}
|
||||
|
|
|
@ -79,6 +79,10 @@ public struct StatusEditorView: View {
|
|||
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
Task {
|
||||
await viewModel.fetchCustomEmojis()
|
||||
}
|
||||
}
|
||||
.onChange(of: currentAccount.account?.id, perform: { _ in
|
||||
viewModel.client = client
|
||||
|
|
|
@ -53,6 +53,9 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
@Published var mediasImages: [ImageContainer] = []
|
||||
@Published var replyToStatus: Status?
|
||||
@Published var embeddedStatus: Status?
|
||||
|
||||
@Published var customEmojis: [Emoji] = []
|
||||
|
||||
var canPost: Bool {
|
||||
statusText.length > 0 || !mediasImages.isEmpty
|
||||
}
|
||||
|
@ -78,26 +81,6 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
self.mode = mode
|
||||
}
|
||||
|
||||
func insertStatusText(text: String) {
|
||||
let string = statusText
|
||||
string.mutableString.insert(text, at: selectedRange.location)
|
||||
statusText = string
|
||||
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 replaceTextWith(text: String) {
|
||||
statusText = .init(string: text)
|
||||
selectedRange = .init(location: text.utf16.count, length: 0)
|
||||
}
|
||||
|
||||
func setInitialLanguageSelection(preference: String?) {
|
||||
switch mode {
|
||||
case let .replyTo(status), let .edit(status):
|
||||
|
@ -109,11 +92,6 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language
|
||||
}
|
||||
|
||||
private func getPollOptionsForAPI() -> [String]? {
|
||||
let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
||||
return options.isEmpty ? nil : options
|
||||
}
|
||||
|
||||
func postStatus() async -> Status? {
|
||||
guard let client else { return nil }
|
||||
do {
|
||||
|
@ -148,6 +126,29 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Status Text manipulations
|
||||
|
||||
func insertStatusText(text: String) {
|
||||
let string = statusText
|
||||
string.mutableString.insert(text, at: selectedRange.location)
|
||||
statusText = string
|
||||
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 replaceTextWith(text: String) {
|
||||
statusText = .init(string: text)
|
||||
selectedRange = .init(location: text.utf16.count, length: 0)
|
||||
}
|
||||
|
||||
func prepareStatusText() {
|
||||
switch mode {
|
||||
case let .new(visibility):
|
||||
|
@ -259,6 +260,7 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
// MARK: - Shar sheet / Item provider
|
||||
private func processItemsProvider(items: [NSItemProvider]) {
|
||||
Task {
|
||||
var initialText: String = ""
|
||||
|
@ -286,12 +288,22 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Polls
|
||||
|
||||
func resetPollDefaults() {
|
||||
pollOptions = ["", ""]
|
||||
pollDuration = .oneDay
|
||||
pollVotingFrequency = .oneVote
|
||||
}
|
||||
|
||||
private func getPollOptionsForAPI() -> [String]? {
|
||||
let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
||||
return options.isEmpty ? nil : options
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Embeds
|
||||
|
||||
private func checkEmbed() {
|
||||
if let url = embeddedStatusURL,
|
||||
!statusText.string.contains(url.absoluteString)
|
||||
|
@ -446,6 +458,14 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
filename: "file",
|
||||
data: data)
|
||||
}
|
||||
|
||||
// MARK: - Custom emojis
|
||||
func fetchCustomEmojis() async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
customEmojis = try await client.get(endpoint: CustomEmojis.customEmojis) ?? []
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusEditorViewModel: DropDelegate {
|
||||
|
|
|
@ -249,7 +249,7 @@ public struct StatusRowView: View {
|
|||
}
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis)
|
||||
.font(.scaledHeadline)
|
||||
.font(.scaledSubheadline)
|
||||
.fontWeight(.semibold)
|
||||
Group {
|
||||
Text("@\(status.account.acct)") +
|
||||
|
|
Loading…
Reference in New Issue