mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +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
@@ -22,6 +22,7 @@ struct RoomScreenCoordinatorParameters {
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let roomName: String?
|
||||
let roomAvatarUrl: String?
|
||||
let emojiProvide: EmojiProviderProtocol
|
||||
}
|
||||
|
||||
final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -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(let itemId):
|
||||
self.displayEmojiPickerScreen(for: itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,4 +111,22 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
private func displayEmojiPickerScreen(for itemId: String) {
|
||||
guard let emojiProvider = parameters?.emojiProvide else {
|
||||
fatalError()
|
||||
}
|
||||
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
|
||||
itemId: itemId)
|
||||
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case let .selectEmoji(emojiId: emojiId, itemId: itemId):
|
||||
self?.navigationController.dismissSheet()
|
||||
MXLog.debug("Save \(emojiId) for \(itemId)")
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.presentSheet(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import UIKit
|
||||
enum RoomScreenViewModelAction {
|
||||
case displayVideo(videoURL: URL)
|
||||
case displayFile(fileURL: URL, title: String?)
|
||||
case displayEmojiPicker(itemId: String)
|
||||
}
|
||||
|
||||
enum RoomScreenComposerMode: Equatable {
|
||||
@@ -29,6 +30,7 @@ enum RoomScreenComposerMode: Equatable {
|
||||
}
|
||||
|
||||
enum RoomScreenViewAction {
|
||||
case displayEmojiPicker(itemId: String)
|
||||
case paginateBackwards
|
||||
case itemAppeared(id: String)
|
||||
case itemDisappeared(id: String)
|
||||
@@ -38,6 +40,7 @@ enum RoomScreenViewAction {
|
||||
case sendReaction(key: String, eventID: String)
|
||||
case cancelReply
|
||||
case cancelEdit
|
||||
case displayReactionsMenuForItemId(itemId: String)
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
@@ -48,6 +51,7 @@ struct RoomScreenViewState: BindableState {
|
||||
var isBackPaginating = false
|
||||
var showLoading = false
|
||||
var bindings: RoomScreenViewStateBindings
|
||||
var displayReactionsMenuForItemId = ""
|
||||
|
||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
var callback: ((RoomScreenViewModelAction) -> Void)?
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
override func process(viewAction: RoomScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .paginateBackwards:
|
||||
@@ -108,6 +109,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
case .sendReaction(let key, _):
|
||||
#warning("Reaction implementation awaiting SDK support.")
|
||||
MXLog.warning("React with \(key) failed. Not implemented.")
|
||||
case .displayEmojiPicker(let itemId):
|
||||
callback?(.displayEmojiPicker(itemId: itemId))
|
||||
case .displayReactionsMenuForItemId(let itemId):
|
||||
state.displayReactionsMenuForItemId = itemId
|
||||
case .cancelReply:
|
||||
state.composerMode = .default
|
||||
case .cancelEdit:
|
||||
|
||||
@@ -19,6 +19,7 @@ import SwiftUI
|
||||
struct RoomScreen: View {
|
||||
@ObservedObject private var settings = ElementSettings.shared
|
||||
@ObservedObject var context: RoomScreenViewModel.Context
|
||||
@State private var showReactionsMenuForItemId = ""
|
||||
|
||||
var body: some View {
|
||||
timeline
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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 TimelineItemReactionsMenuView: View {
|
||||
private let emojis = ["👍🏼", "👎🏼", "😄", "🙏🏼", "😇"]
|
||||
|
||||
var onDisplayEmojiPicker: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(emojis, id: \.self) { emoji in
|
||||
Text(emoji)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(.gray)
|
||||
.cornerRadius(15)
|
||||
HStack(spacing: 10) {
|
||||
Text("➕")
|
||||
}
|
||||
.padding(10)
|
||||
.background(.gray)
|
||||
.cornerRadius(15)
|
||||
.onTapGesture {
|
||||
onDisplayEmojiPicker?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemReactionsMenuView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimelineItemReactionsMenuView(onDisplayEmojiPicker: nil)
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,12 @@ struct TimelineTableView: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
var displayReactionsMenuForItemId = "" {
|
||||
didSet {
|
||||
tableView?.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
/// The table's diffable data source.
|
||||
private var dataSource: UITableViewDiffableDataSource<TimelineSection, RoomTimelineViewProvider>?
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
@@ -175,25 +181,35 @@ struct TimelineTableView: UIViewRepresentable {
|
||||
|
||||
cell.item = timelineItem
|
||||
cell.contentConfiguration = UIHostingConfiguration {
|
||||
timelineItem
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(viewModelContext.viewState.opacity(for: timelineItem))
|
||||
.contextMenu {
|
||||
viewModelContext.viewState.contextMenuBuilder?(timelineItem.id)
|
||||
}
|
||||
.onAppear {
|
||||
viewModelContext.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
.onDisappear {
|
||||
viewModelContext.send(viewAction: .itemDisappeared(id: timelineItem.id))
|
||||
}
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
viewModelContext.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
.onTapGesture {
|
||||
viewModelContext.send(viewAction: .itemTapped(id: timelineItem.id))
|
||||
VStack {
|
||||
if viewModelContext.viewState.displayReactionsMenuForItemId == timelineItem.id {
|
||||
TimelineItemReactionsMenuView {
|
||||
viewModelContext.send(viewAction: .displayEmojiPicker(itemId: timelineItem.id))
|
||||
}
|
||||
}
|
||||
timelineItem
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(viewModelContext.viewState.opacity(for: timelineItem))
|
||||
.contextMenu {
|
||||
viewModelContext.viewState.contextMenuBuilder?(timelineItem.id)
|
||||
}
|
||||
.onAppear {
|
||||
viewModelContext.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
.onDisappear {
|
||||
viewModelContext.send(viewAction: .itemDisappeared(id: timelineItem.id))
|
||||
}
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
viewModelContext.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
.onTapGesture(count: 2) {
|
||||
viewModelContext.send(viewAction: .displayReactionsMenuForItemId(itemId: timelineItem.id))
|
||||
}
|
||||
.onTapGesture {
|
||||
viewModelContext.send(viewAction: .itemTapped(id: timelineItem.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
.margins(.all, self.timelineStyle.rowInsets)
|
||||
.minSize(height: 1)
|
||||
@@ -240,6 +256,9 @@ struct TimelineTableView: UIViewRepresentable {
|
||||
if composerMode != viewModelContext.viewState.composerMode {
|
||||
composerMode = viewModelContext.viewState.composerMode
|
||||
}
|
||||
if displayReactionsMenuForItemId != viewModelContext.viewState.displayReactionsMenuForItemId {
|
||||
displayReactionsMenuForItemId = viewModelContext.viewState.displayReactionsMenuForItemId
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the table view with the latest items from the ``timelineItems`` array. After
|
||||
|
||||
Reference in New Issue
Block a user