diff --git a/Packages/Models/Sources/Models/Emoji.swift b/Packages/Models/Sources/Models/Emoji.swift index 47cf8c76..bc12b82d 100644 --- a/Packages/Models/Sources/Models/Emoji.swift +++ b/Packages/Models/Sources/Models/Emoji.swift @@ -13,4 +13,5 @@ public struct Emoji: Codable, Hashable, Identifiable, Equatable, Sendable { public let url: URL public let staticUrl: URL public let visibleInPicker: Bool + public let category: String? } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index 9afeba8a..ee5ef5bc 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -120,7 +120,7 @@ struct StatusEditorAccessoryView: View { } } - if !viewModel.customEmojis.isEmpty { + if !viewModel.customEmojiContainer.isEmpty { Button { isCustomEmojisSheetDisplay = true } label: { @@ -283,29 +283,37 @@ struct StatusEditorAccessoryView: View { 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 - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 40, height: 40) - .accessibilityLabel(emoji.shortcode.replacingOccurrences(of: "_", with: " ")) - .accessibilityAddTraits(.isButton) - } else if state.isLoading { - Rectangle() - .fill(Color.gray) - .frame(width: 40, height: 40) - .accessibility(hidden: true) - .shimmering() + ForEach(viewModel.customEmojiContainer) { container in + VStack(alignment: .leading) { + Text(container.categoryName) + .font(.scaledFootnote) + LazyVGrid(columns: [GridItem(.adaptive(minimum: 40))], spacing: 9) { + ForEach(container.emojis) { emoji in + LazyImage(url: emoji.url) { state in + if let image = state.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 40, height: 40) + .accessibilityLabel(emoji.shortcode.replacingOccurrences(of: "_", with: " ")) + .accessibilityAddTraits(.isButton) + } else if state.isLoading { + Rectangle() + .fill(Color.gray) + .frame(width: 40, height: 40) + .accessibility(hidden: true) + .shimmering() + } + } + .onTapGesture { + viewModel.insertStatusText(text: " :\(emoji.shortcode): ") + } } } - .onTapGesture { - viewModel.insertStatusText(text: " :\(emoji.shortcode): ") - } } - }.padding(.horizontal) + .padding(.horizontal) + .padding(.bottom) + } } .toolbar { ToolbarItem(placement: .navigationBarLeading) { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorCategorizedEmojiContainer.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorCategorizedEmojiContainer.swift new file mode 100644 index 00000000..2a59a1a6 --- /dev/null +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorCategorizedEmojiContainer.swift @@ -0,0 +1,8 @@ +import Foundation +import Models + +struct StatusEditorCategorizedEmojiContainer: Identifiable, Equatable { + let id = UUID().uuidString + let categoryName: String + var emojis: [Emoji] +} diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 1bf1ffd0..d1785563 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -100,7 +100,7 @@ import SwiftUI var replyToStatus: Status? var embeddedStatus: Status? - var customEmojis: [Emoji] = [] + var customEmojiContainer: [StatusEditorCategorizedEmojiContainer] = [] var postingError: String? var showPostingErrorAlert: Bool = false @@ -726,9 +726,33 @@ import SwiftUI // MARK: - Custom emojis func fetchCustomEmojis() async { + typealias EmojiContainer = StatusEditorCategorizedEmojiContainer + guard let client else { return } do { - customEmojis = try await client.get(endpoint: CustomEmojis.customEmojis) ?? [] + let customEmojis: [Emoji] = try await client.get(endpoint: CustomEmojis.customEmojis) ?? [] + var emojiContainers: [EmojiContainer] = [] + + customEmojis.reduce([String: [Emoji]]()) { currentDict, emoji in + var dict = currentDict + let category = emoji.category ?? "Uncategorized" + + if let emojis = dict[category] { + dict[category] = emojis + [emoji] + } else { + dict[category] = [emoji] + } + + return dict + }.sorted(by: { lhs, rhs in + if rhs.key == "Uncategorized" { return false } + else if lhs.key == "Uncategorized" { return true } + else { return lhs.key < rhs.key } + }).forEach { key, value in + emojiContainers.append(.init(categoryName: key, emojis: value)) + } + + customEmojiContainer = emojiContainers } catch {} } }