mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-25 02:52:45 +02:00
#1098 - Working real user suggestion inside the main application.
This commit is contained in:
+31
-4
@@ -20,6 +20,13 @@ import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol UserSuggestionCoordinatorDelegate: AnyObject {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator,
|
||||
didRequestMentionForMember member: MXRoomMember,
|
||||
textTrigger: String?)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class UserSuggestionCoordinator: Coordinator {
|
||||
|
||||
@@ -30,7 +37,7 @@ final class UserSuggestionCoordinator: Coordinator {
|
||||
private let parameters: UserSuggestionCoordinatorParameters
|
||||
private let userSuggestionHostingController: UIViewController
|
||||
|
||||
private var userSuggestionService: UserSuggestionServiceProtocol
|
||||
private var userSuggestionService: UserSuggestionService
|
||||
private var userSuggestionViewModel: UserSuggestionViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
@@ -39,6 +46,8 @@ final class UserSuggestionCoordinator: Coordinator {
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
weak var delegate: UserSuggestionCoordinatorDelegate?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
@@ -47,15 +56,33 @@ final class UserSuggestionCoordinator: Coordinator {
|
||||
|
||||
userSuggestionService = UserSuggestionService(room: parameters.room)
|
||||
userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
|
||||
|
||||
let view = UserSuggestionList(viewModel: userSuggestionViewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
|
||||
|
||||
userSuggestionHostingController = UIHostingController(rootView: view)
|
||||
|
||||
userSuggestionViewModel.completion = { [weak self] result in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .selectedItemWithIdentifier(let identifier):
|
||||
guard let member = self.userSuggestionService.roomMemberForIdentifier(identifier) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.userSuggestionCoordinator(self,
|
||||
didRequestMentionForMember: member,
|
||||
textTrigger: self.userSuggestionService.currentTextTrigger)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processPartialUserName(_ userName: String) {
|
||||
userSuggestionService.processPartialUserName(userName)
|
||||
func processTextMessage(_ textMessage: String) {
|
||||
userSuggestionService.processTextMessage(textMessage)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
+30
-3
@@ -16,6 +16,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject {
|
||||
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge,
|
||||
didRequestMentionForMember member: MXRoomMember,
|
||||
textTrigger: String?)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
|
||||
@@ -25,16 +32,25 @@ final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
return _userSuggestionCoordinator as! UserSuggestionCoordinator
|
||||
}
|
||||
|
||||
weak var delegate: UserSuggestionCoordinatorBridgeDelegate?
|
||||
|
||||
init(mediaManager: MXMediaManager, room: MXRoom) {
|
||||
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room)
|
||||
if #available(iOS 14.0, *) {
|
||||
self._userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters)
|
||||
let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters)
|
||||
self._userSuggestionCoordinator = userSuggestionCoordinator
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
userSuggestionCoordinator.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
func processPartialUserName(_ userName: String) {
|
||||
func processTextMessage(_ textMessage: String) {
|
||||
if #available(iOS 14.0, *) {
|
||||
return self.userSuggestionCoordinator.processPartialUserName(userName)
|
||||
return self.userSuggestionCoordinator.processTextMessage(textMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +62,14 @@ final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator,
|
||||
didRequestMentionForMember member: MXRoomMember,
|
||||
textTrigger: String?) {
|
||||
delegate?.userSuggestionCoordinatorBridge(self,
|
||||
didRequestMentionForMember: member,
|
||||
textTrigger: textTrigger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ import Foundation
|
||||
|
||||
@available(iOS 14, *)
|
||||
enum UserSuggestionViewModelResult {
|
||||
case selectedItem(UserSuggestionItemProtocol)
|
||||
case selectedItemWithIdentifier(String)
|
||||
}
|
||||
|
||||
+61
-14
@@ -19,6 +19,13 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionServiceItem: UserSuggestionItemProtocol {
|
||||
let userId: String
|
||||
let displayName: String?
|
||||
let avatarUrl: String?
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
|
||||
@@ -29,10 +36,12 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
private let room: MXRoom
|
||||
|
||||
private var suggestionItems: [UserSuggestionItemProtocol] = []
|
||||
private var roomJoinedMembers: [MXRoomMember] = []
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never>
|
||||
var currentTextTrigger: String?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -40,26 +49,64 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
self.room = room
|
||||
self.items = CurrentValueSubject([])
|
||||
|
||||
generateUsersWithCount(10)
|
||||
items.send(suggestionItems)
|
||||
self.room.members { [weak self] members in
|
||||
guard let self = self, let joinedMembers = members?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
|
||||
self.roomJoinedMembers = joinedMembers
|
||||
|
||||
self.suggestionItems = joinedMembers.map { member in
|
||||
UserSuggestionServiceItem(userId: member.userId, displayName: member.displayname, avatarUrl: member.avatarUrl)
|
||||
}
|
||||
} lazyLoadedMembers: { [weak self] lazyMembers in
|
||||
guard let self = self, let joinedMembers = lazyMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
|
||||
self.roomJoinedMembers = joinedMembers
|
||||
|
||||
self.suggestionItems = joinedMembers.map { member in
|
||||
UserSuggestionServiceItem(userId: member.userId, displayName: member.displayname, avatarUrl: member.avatarUrl)
|
||||
}
|
||||
} failure: { error in
|
||||
MXLog.error("[UserSuggestionService] Failed loading room with error: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
|
||||
func processPartialUserName(_ userName: String) {
|
||||
guard userName.count > 0 else {
|
||||
items.send(suggestionItems)
|
||||
func roomMemberForIdentifier(_ identifier: String) -> MXRoomMember? {
|
||||
return roomJoinedMembers.filter { $0.userId == identifier }.first
|
||||
}
|
||||
|
||||
// MARK: - UserSuggestionServiceProtocol
|
||||
|
||||
func processTextMessage(_ textMessage: String) {
|
||||
items.send([])
|
||||
currentTextTrigger = nil
|
||||
|
||||
guard textMessage.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
let components = textMessage.components(separatedBy: .whitespaces)
|
||||
|
||||
guard let lastComponent = components.last else {
|
||||
return
|
||||
}
|
||||
|
||||
guard lastComponent.hasPrefix("@") else {
|
||||
return
|
||||
}
|
||||
|
||||
currentTextTrigger = lastComponent
|
||||
|
||||
var partialName = lastComponent
|
||||
partialName.removeFirst()
|
||||
|
||||
items.send(suggestionItems.filter({ userSuggestion in
|
||||
return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none)
|
||||
let containedInUsername = userSuggestion.userId.lowercased().range(of: partialName.lowercased()) != .none
|
||||
let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().range(of: partialName.lowercased()) != .none
|
||||
return (containedInUsername || containedInDisplayName)
|
||||
}))
|
||||
}
|
||||
|
||||
private func generateUsersWithCount(_ count: UInt) {
|
||||
suggestionItems.removeAll()
|
||||
for _ in 0..<count {
|
||||
let identifier = "@" + UUID().uuidString
|
||||
suggestionItems.append(MockUserSuggestionServiceItem(userId: identifier, displayName: identifier, avatarUrl: "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service)
|
||||
|
||||
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { partialUserName in
|
||||
service.processPartialUserName(partialUserName)
|
||||
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
|
||||
service.processTextMessage(textMessage)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+19
-4
@@ -38,14 +38,29 @@ class MockUserSuggestionService: UserSuggestionServiceProtocol {
|
||||
items.send(suggestionItems)
|
||||
}
|
||||
|
||||
func processPartialUserName(_ userName: String) {
|
||||
guard userName.count > 0 else {
|
||||
items.send(suggestionItems)
|
||||
func processTextMessage(_ textMessage: String) {
|
||||
|
||||
items.send([])
|
||||
|
||||
guard textMessage.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
let components = textMessage.components(separatedBy: .whitespaces)
|
||||
|
||||
guard let lastComponent = components.last else {
|
||||
return
|
||||
}
|
||||
|
||||
guard lastComponent.hasPrefix("@") else {
|
||||
return
|
||||
}
|
||||
|
||||
var partialName = lastComponent
|
||||
partialName.removeFirst()
|
||||
|
||||
items.send(suggestionItems.filter({ userSuggestion in
|
||||
return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none)
|
||||
return (userSuggestion.displayName?.lowercased().range(of: partialName.lowercased()) != .none)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ protocol UserSuggestionServiceProtocol {
|
||||
|
||||
var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get }
|
||||
|
||||
func processPartialUserName(_ userName: String)
|
||||
func processTextMessage(_ textMessage: String)
|
||||
}
|
||||
|
||||
// MARK: Avatarable
|
||||
|
||||
@@ -36,28 +36,20 @@ struct UserSuggestionList: View {
|
||||
|
||||
var body: some View {
|
||||
BackgroundView {
|
||||
ScrollViewReader { scrollViewReader in
|
||||
List(viewModel.viewState.items) { item in
|
||||
List(viewModel.viewState.items) { item in
|
||||
Button {
|
||||
viewModel.send(viewAction: .selectedItem(item))
|
||||
} label: {
|
||||
UserSuggestionListItem(
|
||||
avatar: item.avatar,
|
||||
displayName: item.displayName,
|
||||
userId: item.id
|
||||
)
|
||||
.padding([.top, .bottom], 4.0)
|
||||
.onTapGesture {
|
||||
viewModel.send(viewAction: .selectedItem(item))
|
||||
}
|
||||
}
|
||||
.environment(\.defaultMinListRowHeight, rowHeight)
|
||||
.frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count)))
|
||||
.onAppear(perform: {
|
||||
guard let lastItemId = viewModel.viewState.items.last?.id else {
|
||||
return
|
||||
}
|
||||
|
||||
scrollViewReader.scrollTo(lastItemId)
|
||||
})
|
||||
}
|
||||
.environment(\.defaultMinListRowHeight, rowHeight)
|
||||
.frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,6 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
|
||||
return UserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
}
|
||||
|
||||
deinit {
|
||||
print("well shit")
|
||||
}
|
||||
|
||||
private init(userSuggestionService: UserSuggestionServiceProtocol) {
|
||||
self.userSuggestionService = userSuggestionService
|
||||
super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService))
|
||||
@@ -71,8 +67,8 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
|
||||
|
||||
override func process(viewAction: UserSuggestionViewAction) {
|
||||
switch viewAction {
|
||||
case .selectedItem(_):
|
||||
break
|
||||
case .selectedItem(let item):
|
||||
completion?(.selectedItemWithIdentifier(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user