Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeline Reactions: Emoji picker #350

Merged
merged 21 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 119 additions & 31 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2347,3 +2347,11 @@
"onboarding_new_app_layout_feedback_title" = "Give Feedback";
"onboarding_new_app_layout_feedback_message" = "Tap top right to see the option to feedback.";
"onboarding_new_app_layout_button_try" = "Try it out";
"emoji_picker_people_category" = "Smileys & People";
"emoji_picker_nature_category" = "Animals & Nature";
"emoji_picker_foods_category" = "Food & Drink";
"emoji_picker_activity_category" = "Activities";
"emoji_picker_places_category" = "Travel & Places";
"emoji_picker_objects_category" = "Objects";
"emoji_picker_symbols_category" = "Symbols";
"emoji_picker_flags_category" = "Flags";
16 changes: 16 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,22 @@ public enum ElementL10n {
public static var editPollTitle: String { return ElementL10n.tr("Localizable", "edit_poll_title") }
/// (edited)
public static var editedSuffix: String { return ElementL10n.tr("Localizable", "edited_suffix") }
/// Activities
public static var emojiPickerActivityCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_activity_category") }
/// Flags
public static var emojiPickerFlagsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_flags_category") }
/// Food & Drink
public static var emojiPickerFoodsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_foods_category") }
/// Animals & Nature
public static var emojiPickerNatureCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_nature_category") }
/// Objects
public static var emojiPickerObjectsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_objects_category") }
/// Smileys & People
public static var emojiPickerPeopleCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_people_category") }
/// Travel & Places
public static var emojiPickerPlacesCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_places_category") }
/// Symbols
public static var emojiPickerSymbolsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_symbols_category") }
/// Your contact book is empty
public static var emptyContactBook: String { return ElementL10n.tr("Localizable", "empty_contact_book") }
/// Encrypted message
Expand Down
6 changes: 6 additions & 0 deletions ElementX/Sources/Other/Extensions/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,9 @@ extension String {
return mutableString.trimmingCharacters(in: .whitespaces)
}
}

extension String {
func containsIgnoringCase(string: String) -> Bool{
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
range(of: string, options: .caseInsensitive) != nil
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// 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 SwiftUI

struct EmojiPickerScreenCoordinatorParameters {
let emojisProvider: EmojisProviderProtocol
}

enum EmojiPickerScreenCoordinatorAction { }

final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
private let parameters: EmojiPickerScreenCoordinatorParameters
private var viewModel: EmojiPickerScreenViewModelProtocol

var callback: ((EmojiPickerScreenCoordinatorAction) -> Void)?

init(parameters: EmojiPickerScreenCoordinatorParameters) {
self.parameters = parameters

viewModel = EmojiPickerScreenViewModel(emojisProvider: parameters.emojisProvider)
}

func start() {
viewModel.callback = { [weak self] action in
guard let self else { return }
MXLog.debug("EmojiPickerScreenViewModel did complete with result: \(action).")
}
}

func toPresentable() -> AnyView {
AnyView(EmojiPickerScreenScreen(context: viewModel.context)
.presentationDetents([.medium, .large]))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// 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

enum EmojiPickerScreenViewModelAction { }

struct EmojiPickerScreenViewState: BindableState {
var categories: [EmojiPickerEmojiCategoryViewData]
}

enum EmojiPickerScreenViewAction {
case search(searchString: String)
case emojiSelected(emoji: EmojiPickerEmojiViewData)
}

struct EmojiPickerEmojiCategoryViewData: Identifiable {
let id: String
let emojis: [EmojiPickerEmojiViewData]

var name: String {
let categoryNameLocalizationKey = "emoji_picker_\(id)_category"
return ElementL10n.tr("Localizable", categoryNameLocalizationKey)
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
}
}

struct EmojiPickerEmojiViewData: Identifiable {
var id: String
let value: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// 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 SwiftUI

typealias EmojiPickerScreenViewModelType = StateStoreViewModel<EmojiPickerScreenViewState, EmojiPickerScreenViewAction>

class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol {
var callback: ((EmojiPickerScreenViewModelAction) -> Void)?

private let emojisProvider: EmojisProviderProtocol

init(emojisProvider: EmojisProviderProtocol) {
let initialViewState = EmojiPickerScreenViewState(categories: [])
self.emojisProvider = emojisProvider
super.init(initialViewState: initialViewState)
loadEmojis()
}

private func loadEmojis() {
Task(priority: .userInitiated) { [weak self] in
let categories = await emojisProvider.getCategories(searchString: nil)
self?.state.categories = convert(emojiCategories: categories)
}
}

private func convert(emojiCategories: [EmojiCategory]) -> [EmojiPickerEmojiCategoryViewData] {
emojiCategories.compactMap { emojiCategory in

let emojisViewData: [EmojiPickerEmojiViewData] = emojiCategory.emojis.compactMap { emojiItem in

guard let firstSkin = emojiItem.skins.first else {
return nil
}
return EmojiPickerEmojiViewData(id: emojiItem.id, value: firstSkin.value)
}

return EmojiPickerEmojiCategoryViewData(id: emojiCategory.id, emojis: emojisViewData)
}
}

override func process(viewAction: EmojiPickerScreenViewAction) async {
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
switch viewAction {
case let .search(searchString: searchString):
let categories = await emojisProvider.getCategories(searchString: searchString)
state.categories = convert(emojiCategories: categories)
case .emojiSelected:
break
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// 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 EmojiPickerScreenViewModelProtocol {
var callback: ((EmojiPickerScreenViewModelAction) -> Void)? { get set }
var context: EmojiPickerScreenViewModelType.Context { get }
}
Original file line number Diff line number Diff line change
@@ -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 SwiftUI

struct EmojiPickerHeaderView: View {
let title: String

var body: some View {
HStack {
Text(title)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}

struct EmojiPickerHeaderView_Previews: PreviewProvider {
static var previews: some View {
Group {
EmojiPickerHeaderView(title: "")
}
}
}
Original file line number Diff line number Diff line change
@@ -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 SwiftUI

struct EmojiPickerScreenScreen: View {
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
@Environment(\.colorScheme) private var colorScheme
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved

@ObservedObject var context: EmojiPickerScreenViewModel.Context
@State var searchString = ""

var body: some View {
VStack {
EmojiPickerSearchFieldView(searchString: $searchString)
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
.padding(.horizontal, 10)
.padding(.top, 10)
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 45))], spacing: 3) {
ForEach(context.viewState.categories) { category in
Section(header: EmojiPickerHeaderView(title: category.name)
.padding(.horizontal, 13)
.padding(.top, 10)) {
ForEach(category.emojis) { emoji in
Text(emoji.value)
.frame(width: 45, height: 45)
.onTapGesture {
context.send(viewAction: .emojiSelected(emoji: emoji))
}
}
}
}
}
}
}
.onChange(of: searchString) { _ in
context.send(viewAction: .search(searchString: searchString))
}
}
}

// MARK: - Previews

struct EmojiPickerScreen_Previews: PreviewProvider {
static var previews: some View {
EmojiPickerScreenScreen(context: EmojiPickerScreenViewModel(emojisProvider: EmojisProvider()).context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// 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 SwiftUI

struct EmojiPickerSearchFieldView: View {
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
@Binding var searchString: String

var body: some View {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search", text: $searchString)
}
}
}

struct EmojiPickerSearchFieldView_Previews: PreviewProvider {
static var previews: some View {
EmojiPickerSearchFieldView(searchString: .constant(""))
}
}
12 changes: 12 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct RoomScreenCoordinatorParameters {
let mediaProvider: MediaProviderProtocol
let roomName: String?
let roomAvatarUrl: String?
let emojisProvide: EmojisProviderProtocol
paleksandrs marked this conversation as resolved.
Show resolved Hide resolved
}

final class RoomScreenCoordinator: CoordinatorProtocol {
Expand Down Expand Up @@ -57,6 +58,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
self.displayVideo(for: videoURL)
case .displayFile(let fileURL, let title):
self.displayFile(for: fileURL, with: title)
case .displayEmojiPicker:
self.displayEmojiPickerScreen()
}
}
}
Expand Down Expand Up @@ -108,4 +111,13 @@ final class RoomScreenCoordinator: CoordinatorProtocol {

navigationController.push(coordinator)
}

private func displayEmojiPickerScreen() {
guard let emojisProvider = parameters?.emojisProvide else {
fatalError()
}
let params = EmojiPickerScreenCoordinatorParameters(emojisProvider: emojisProvider)
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
navigationController.presentSheet(coordinator)
}
}
Loading