mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Timeline Reactions: Emoji picker (#350)
* Screens template * Loading and parsing * Gridview * New json * Search method * Unit tests * Added emoji provider state * Removed containsIgnoringCase * Added plus icon, added double tap gesture, dismiss sheet after selecting emoji * Renamed EmojisProvider to EmojiProvider * Renamed EmojiPickerScreenScreen * Extra padding * Renamed EmojisLoaderProtocol * Category names * Moved method into public section of the class * Return all categories when search string is empty * Added cancel button and tittle * Removed empty lines * Removed swiftlint warning
This commit is contained in:
committed by
GitHub
parent
dcedb3fb98
commit
7f91852291
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
struct EmojiCategory: Equatable, Identifiable {
|
||||
let id: String
|
||||
let emojis: [EmojiItem]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmojiItem: Equatable, Identifiable {
|
||||
var id: String
|
||||
let name: String
|
||||
let keywords: [String]
|
||||
let skins: [EmojiItemSkin]
|
||||
}
|
||||
|
||||
extension EmojiItem {
|
||||
init?(from emojiMart: EmojiMartEmoji) {
|
||||
id = emojiMart.id
|
||||
name = emojiMart.name
|
||||
keywords = emojiMart.keywords
|
||||
skins = emojiMart.skins.compactMap { emojiMartEmojiSkin in
|
||||
EmojiItemSkin(from: emojiMartEmojiSkin)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmojiItemSkin: Equatable {
|
||||
let value: String
|
||||
|
||||
init?(from emojiMartEmojiSkin: EmojiMartEmojiSkin) {
|
||||
let unicodeStringComponents = emojiMartEmojiSkin.unified.components(separatedBy: "-")
|
||||
|
||||
var emoji = ""
|
||||
|
||||
for unicodeStringComponent in unicodeStringComponents {
|
||||
guard let unicodeCodePoint = Int(unicodeStringComponent, radix: 16),
|
||||
let emojiUnicodeScalar = UnicodeScalar(unicodeCodePoint) else {
|
||||
return nil
|
||||
}
|
||||
emoji.append(String(emojiUnicodeScalar))
|
||||
}
|
||||
value = emoji
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol EmojiLoaderProtocol {
|
||||
func load() async -> [EmojiCategory]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmojiMartCategory: Decodable {
|
||||
let id: String
|
||||
let emojis: [String]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmojiMartEmoji: Decodable {
|
||||
let id: String
|
||||
let name: String
|
||||
let keywords: [String]
|
||||
let skins: [EmojiMartEmojiSkin]
|
||||
}
|
||||
|
||||
struct EmojiMartEmojiSkin: Decodable {
|
||||
let unified: String
|
||||
let native: String
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private enum EmojiMartJSONLoaderError: Error {
|
||||
case fileNotFound
|
||||
}
|
||||
|
||||
class EmojiMartJSONLoader: EmojiLoaderProtocol {
|
||||
/// Emoji data coming from https://github.com/missive/emoji-mart/blob/main/packages/emoji-mart-data/sets/14/apple.json
|
||||
private let jsonFilename = "apple_emojis_data"
|
||||
|
||||
func load() async -> [EmojiCategory] {
|
||||
do {
|
||||
let data = try await loadJSONData()
|
||||
let store = try await decodeJSONData(data: data)
|
||||
return emojiCategories(from: store)
|
||||
} catch {
|
||||
MXLog.error("Couldn't parse emoji json")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func loadJSONData() async throws -> Data {
|
||||
guard let jsonDataURL = Bundle.main.url(forResource: jsonFilename, withExtension: "json") else {
|
||||
throw EmojiMartJSONLoaderError.fileNotFound
|
||||
}
|
||||
return try Data(contentsOf: jsonDataURL)
|
||||
}
|
||||
|
||||
private func decodeJSONData(data: Data) async throws -> EmojiMartStore {
|
||||
try JSONDecoder().decode(EmojiMartStore.self, from: data)
|
||||
}
|
||||
|
||||
private func emojiCategories(from emojiMartStore: EmojiMartStore) -> [EmojiCategory] {
|
||||
emojiMartStore.categories.map { emojiMartCategory -> EmojiCategory in
|
||||
let emojiItems = emojiMartCategory.emojis.compactMap { emoji -> EmojiItem? in
|
||||
guard let emojiMartEmoji = emojiMartStore.emojis.first(where: { $0.id == emoji }) else {
|
||||
return nil
|
||||
}
|
||||
return EmojiItem(from: emojiMartEmoji)
|
||||
}
|
||||
return EmojiCategory(id: emojiMartCategory.id, emojis: emojiItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmojiMartStore: Decodable {
|
||||
let categories: [EmojiMartCategory]
|
||||
let emojis: [EmojiMartEmoji]
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case categories
|
||||
case emojis
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
categories = try container.decode([EmojiMartCategory].self, forKey: .categories)
|
||||
|
||||
let emojisDictionary = try container.decode([String: EmojiMartEmoji].self, forKey: .emojis)
|
||||
emojis = emojisDictionary.map(\.value)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol EmojiProviderProtocol {
|
||||
func getCategories(searchString: String?) async -> [EmojiCategory]
|
||||
}
|
||||
|
||||
private enum EmojiProviderState {
|
||||
case notLoaded
|
||||
case inProgress(Task<[EmojiCategory], Never>)
|
||||
case loaded([EmojiCategory])
|
||||
}
|
||||
|
||||
class EmojiProvider: EmojiProviderProtocol {
|
||||
private let loader: EmojiLoaderProtocol
|
||||
private var state: EmojiProviderState = .notLoaded
|
||||
|
||||
init(loader: EmojiLoaderProtocol = EmojiMartJSONLoader()) {
|
||||
self.loader = loader
|
||||
Task {
|
||||
await loadIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func getCategories(searchString: String? = nil) async -> [EmojiCategory] {
|
||||
let emojiCategories = await loadIfNeeded()
|
||||
if let searchString = searchString, searchString.isEmpty == false {
|
||||
return search(searchString: searchString, emojiCategories: emojiCategories)
|
||||
} else {
|
||||
return emojiCategories
|
||||
}
|
||||
}
|
||||
|
||||
private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] {
|
||||
emojiCategories.compactMap { category in
|
||||
let emojis = category.emojis.filter { emoji in
|
||||
let searchArray = [emoji.id, emoji.name] + emoji.keywords
|
||||
return searchArray.description.range(of: searchString, options: .caseInsensitive) != nil
|
||||
}
|
||||
return emojis.isEmpty ? nil : EmojiCategory(id: category.id, emojis: emojis)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadIfNeeded() async -> [EmojiCategory] {
|
||||
switch state {
|
||||
case .notLoaded:
|
||||
let task = Task {
|
||||
await loader.load()
|
||||
}
|
||||
state = .inProgress(task)
|
||||
let categories = await task.value
|
||||
state = .loaded(categories)
|
||||
return categories
|
||||
case .loaded(let categories):
|
||||
return categories
|
||||
case .inProgress(let task):
|
||||
return await task.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let navigationController: NavigationController
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let emojiProvider: EmojiProviderProtocol = EmojiProvider()
|
||||
|
||||
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
|
||||
|
||||
@@ -148,7 +149,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
roomAvatarUrl: roomProxy.avatarURL)
|
||||
roomAvatarUrl: roomProxy.avatarURL,
|
||||
emojiProvide: emojiProvider)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationController.push(coordinator) { [weak self] in
|
||||
|
||||
Reference in New Issue
Block a user