mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-19 19:16:16 +00:00
Add support for custom emojis in the composer close #98
This commit is contained in:
parent
fd6f337571
commit
9c532d9448
6 changed files with 117 additions and 32 deletions
16
Packages/Network/Sources/Network/Endpoint/CustomEmojis.swift
Normal file
16
Packages/Network/Sources/Network/Endpoint/CustomEmojis.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)") +
|
||||||
|
|
Loading…
Reference in a new issue