diff --git a/Packages/Network/Sources/Network/Endpoint/CustomEmojis.swift b/Packages/Network/Sources/Network/Endpoint/CustomEmojis.swift new file mode 100644 index 00000000..1889340a --- /dev/null +++ b/Packages/Network/Sources/Network/Endpoint/CustomEmojis.swift @@ -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 + } +} diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index cd1f4c42..8d473bfc 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -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 { @@ -51,6 +53,14 @@ struct StatusEditorAccessoryView: View { Image(systemName: "archivebox") } } + + if !viewModel.customEmojis.isEmpty { + Button { + isCustomEmojisSheetDisplay = true + } label: { + Image(systemName: "face.smiling.inverse") + } + } Button { isLanguageSheetDisplayed.toggle() @@ -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) } @@ -154,9 +167,41 @@ 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) } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift index e5802b07..a38c9b33 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift @@ -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 } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 0aa8cf31..eab54606 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -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 diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 15444950..eabeea38 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -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): @@ -194,7 +195,7 @@ public class StatusEditorViewModel: ObservableObject { } } } - + private func processText() { statusText.addAttributes([.foregroundColor: UIColor(Color.label)], range: NSMakeRange(0, statusText.string.utf16.count)) @@ -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 { diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 52592e7a..4f71c23a 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -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)") +