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 Models
import PhotosUI import PhotosUI
import SwiftUI import SwiftUI
import NukeUI
struct StatusEditorAccessoryView: View { struct StatusEditorAccessoryView: View {
@EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var preferences: UserPreferences
@ -14,6 +15,7 @@ struct StatusEditorAccessoryView: View {
@State private var isDraftsSheetDisplayed: Bool = false @State private var isDraftsSheetDisplayed: Bool = false
@State private var isLanguageSheetDisplayed: Bool = false @State private var isLanguageSheetDisplayed: Bool = false
@State private var isCustomEmojisSheetDisplay: Bool = false
@State private var languageSearch: String = "" @State private var languageSearch: String = ""
var body: some View { var body: some View {
@ -51,6 +53,14 @@ struct StatusEditorAccessoryView: View {
Image(systemName: "archivebox") Image(systemName: "archivebox")
} }
} }
if !viewModel.customEmojis.isEmpty {
Button {
isCustomEmojisSheetDisplay = true
} label: {
Image(systemName: "face.smiling.inverse")
}
}
Button { Button {
isLanguageSheetDisplayed.toggle() isLanguageSheetDisplayed.toggle()
@ -74,9 +84,12 @@ struct StatusEditorAccessoryView: View {
.sheet(isPresented: $isDraftsSheetDisplayed) { .sheet(isPresented: $isDraftsSheetDisplayed) {
draftsSheetView draftsSheetView
} }
.sheet(isPresented: $isLanguageSheetDisplayed, content: { .sheet(isPresented: $isLanguageSheetDisplayed) {
languageSheetView languageSheetView
}) }
.sheet(isPresented: $isCustomEmojisSheetDisplay) {
customEmojisSheet
}
.onAppear { .onAppear {
viewModel.setInitialLanguageSelection(preference: preferences.serverPreferences?.postLanguage) viewModel.setInitialLanguageSelection(preference: preferences.serverPreferences?.postLanguage)
} }
@ -154,9 +167,41 @@ struct StatusEditorAccessoryView: View {
} }
.presentationDetents([.medium]) .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 { 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) .foregroundColor(.gray)
.font(.scaledCallout) .font(.scaledCallout)
} }

View file

@ -39,7 +39,7 @@ struct StatusEditorPollView: View {
} }
} }
.onChange(of: viewModel.pollOptions[index]) { .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)) viewModel.pollOptions[index] = String($0.prefix(maxCharacters))
} }
@ -118,7 +118,7 @@ struct StatusEditorPollView: View {
private func canAddMoreAt(_ index: Int) -> Bool { private func canAddMoreAt(_ index: Int) -> Bool {
let count = viewModel.pollOptions.count 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 return index == count - 1 && count < maxEntries
} }

View file

@ -79,6 +79,10 @@ public struct StatusEditorView: View {
NotificationCenter.default.post(name: NotificationsName.shareSheetClose, NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
object: nil) object: nil)
} }
Task {
await viewModel.fetchCustomEmojis()
}
} }
.onChange(of: currentAccount.account?.id, perform: { _ in .onChange(of: currentAccount.account?.id, perform: { _ in
viewModel.client = client viewModel.client = client

View file

@ -53,6 +53,9 @@ public class StatusEditorViewModel: ObservableObject {
@Published var mediasImages: [ImageContainer] = [] @Published var mediasImages: [ImageContainer] = []
@Published var replyToStatus: Status? @Published var replyToStatus: Status?
@Published var embeddedStatus: Status? @Published var embeddedStatus: Status?
@Published var customEmojis: [Emoji] = []
var canPost: Bool { var canPost: Bool {
statusText.length > 0 || !mediasImages.isEmpty statusText.length > 0 || !mediasImages.isEmpty
} }
@ -78,26 +81,6 @@ public class StatusEditorViewModel: ObservableObject {
self.mode = mode 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?) { func setInitialLanguageSelection(preference: String?) {
switch mode { switch mode {
case let .replyTo(status), let .edit(status): case let .replyTo(status), let .edit(status):
@ -109,11 +92,6 @@ public class StatusEditorViewModel: ObservableObject {
selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language 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? { func postStatus() async -> Status? {
guard let client else { return nil } guard let client else { return nil }
do { 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() { func prepareStatusText() {
switch mode { switch mode {
case let .new(visibility): case let .new(visibility):
@ -194,7 +195,7 @@ public class StatusEditorViewModel: ObservableObject {
} }
} }
} }
private func processText() { private func processText() {
statusText.addAttributes([.foregroundColor: UIColor(Color.label)], statusText.addAttributes([.foregroundColor: UIColor(Color.label)],
range: NSMakeRange(0, statusText.string.utf16.count)) range: NSMakeRange(0, statusText.string.utf16.count))
@ -259,6 +260,7 @@ public class StatusEditorViewModel: ObservableObject {
} catch {} } catch {}
} }
// MARK: - Shar sheet / Item provider
private func processItemsProvider(items: [NSItemProvider]) { private func processItemsProvider(items: [NSItemProvider]) {
Task { Task {
var initialText: String = "" var initialText: String = ""
@ -286,12 +288,22 @@ public class StatusEditorViewModel: ObservableObject {
} }
} }
// MARK: - Polls
func resetPollDefaults() { func resetPollDefaults() {
pollOptions = ["", ""] pollOptions = ["", ""]
pollDuration = .oneDay pollDuration = .oneDay
pollVotingFrequency = .oneVote 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() { private func checkEmbed() {
if let url = embeddedStatusURL, if let url = embeddedStatusURL,
!statusText.string.contains(url.absoluteString) !statusText.string.contains(url.absoluteString)
@ -446,6 +458,14 @@ public class StatusEditorViewModel: ObservableObject {
filename: "file", filename: "file",
data: data) 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 { extension StatusEditorViewModel: DropDelegate {

View file

@ -249,7 +249,7 @@ public struct StatusRowView: View {
} }
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis) EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis)
.font(.scaledHeadline) .font(.scaledSubheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {
Text("@\(status.account.acct)") + Text("@\(status.account.acct)") +