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 all 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
120 changes: 120 additions & 0 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// 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 emojiProvider: EmojiProviderProtocol
let itemId: String
}

enum EmojiPickerScreenCoordinatorAction {
case selectEmoji(emojiId: String, itemId: String)
}

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

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

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

viewModel = EmojiPickerScreenViewModel(emojiProvider: parameters.emojiProvider)
}

func start() {
viewModel.callback = { [weak self] action in
guard let self else { return }
MXLog.debug("EmojiPickerScreenViewModel did complete with result: \(action).")
switch action {
case let .selectEmoji(emojiId: emojiId):
self.callback?(.selectEmoji(emojiId: emojiId, itemId: self.parameters.itemId))
}
}
}

func toPresentable() -> AnyView {
AnyView(EmojiPickerScreen(context: viewModel.context)
.presentationDetents([.medium, .large]))
}
}
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 Foundation

enum EmojiPickerScreenViewModelAction {
case selectEmoji(emojiId: String)
}

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 {
switch id {
case "people":
return ElementL10n.emojiPickerPeopleCategory
case "nature":
return ElementL10n.emojiPickerNatureCategory
case "foods":
return ElementL10n.emojiPickerFoodsCategory
case "activity":
return ElementL10n.emojiPickerActivityCategory
case "places":
return ElementL10n.emojiPickerPlacesCategory
case "objects":
return ElementL10n.emojiPickerObjectsCategory
case "symbols":
return ElementL10n.emojiPickerSymbolsCategory
case "flags":
return ElementL10n.emojiPickerFlagsCategory
default:
MXLog.failure("Missing translation for emoji category with id \(id)")
return ""
}
}
}

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

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

// MARK: - Public

override func process(viewAction: EmojiPickerScreenViewAction) async {
switch viewAction {
case let .search(searchString: searchString):
let categories = await emojiProvider.getCategories(searchString: searchString)
state.categories = convert(emojiCategories: categories)
case let .emojiSelected(emoji: emoji):
callback?(.selectEmoji(emojiId: emoji.id))
}
}

// MARK: - Private

private func loadEmojis() {
Task(priority: .userInitiated) { [weak self] in
let categories = await emojiProvider.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)
}
}
}
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,59 @@
//
// 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 EmojiPickerScreen: View {
@ObservedObject var context: EmojiPickerScreenViewModel.Context
@State var searchString = ""

var body: some View {
VStack {
Text(ElementL10n.reactions)
.padding(.top, 20)
EmojiPickerSearchFieldView(searchString: $searchString)
.padding(.horizontal, 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 {
EmojiPickerScreen(context: EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()).context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// 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
@FocusState private var isSearchFocused: Bool

var body: some View {
HStack {
Image(systemName: "magnifyingglass")
TextField(ElementL10n.search, text: $searchString)
.focused($isSearchFocused)
if isSearchFocused {
Spacer()
Button {
searchString = ""
isSearchFocused = false
} label: {
Text(ElementL10n.actionCancel)
}
}
}
}
}

struct EmojiPickerSearchFieldView_Previews: PreviewProvider {
static var previews: some View {
EmojiPickerSearchFieldView(searchString: .constant(""))
}
}
Loading