Add support for custom emojis in the composer close #98

This commit is contained in:
Thomas Ricouard 2023-01-18 19:11:52 +01:00
parent fd6f337571
commit 9c532d9448
6 changed files with 117 additions and 32 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)") +