diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj index 210245ac97411816141b7e9486f7037872a43bea..fd97823ed4b95159425f7d90e5f49cb670deb6c6 100644 --- a/Ring/Ring.xcodeproj/project.pbxproj +++ b/Ring/Ring.xcodeproj/project.pbxproj @@ -324,6 +324,7 @@ 649AD3C324B4CFC700A0236D /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144724B4ACDE007203D5 /* libsqlite3.tbd */; }; 649AD3C624B4CFD500A0236D /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144524B4ACC8007203D5 /* libxml2.tbd */; }; 649AD3C724B4D00100A0236D /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144124B4AC9F007203D5 /* libc++.tbd */; }; + 64DBCD2224DB3CF600CB5CA2 /* UserSearchResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 64DBCD2124DB3CF600CB5CA2 /* UserSearchResponse.m */; }; 64F8127724B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F8127324B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift */; }; 64F8127824B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib in Resources */ = {isa = PBXBuildFile; fileRef = 64F8127624B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib */; }; 64F8127A24BBC19C00A7DE6A /* MessageCellLocationSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F8127924BBC19C00A7DE6A /* MessageCellLocationSharing.swift */; }; @@ -618,8 +619,6 @@ 1A3CA32A1F102BB700283748 /* Chameleon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Chameleon.framework; path = Carthage/Build/iOS/Chameleon.framework; sourceTree = "<group>"; }; 1A3D28A61F0EB9DB00B524EE /* Bool+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bool+String.swift"; sourceTree = "<group>"; }; 1A3D28A81F0EBF0200B524EE /* UIView+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Ring.swift"; sourceTree = "<group>"; }; - 1A5DC00A1F3558980075E8EF /* ContactsAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContactsAdapter.h; sourceTree = "<group>"; }; - 1A5DC00D1F3559070075E8EF /* ContactsAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContactsAdapter.mm; sourceTree = "<group>"; }; 1A5DC01D1F355DA70075E8EF /* ContactsAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsAdapterDelegate.swift; sourceTree = "<group>"; }; 1A5DC01F1F355DCF0075E8EF /* ContactsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsService.swift; sourceTree = "<group>"; }; 1A5DC0231F3564360075E8EF /* ContactRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactRequestModel.swift; sourceTree = "<group>"; }; @@ -688,8 +687,6 @@ 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LookupNameResponse.m; sourceTree = "<group>"; }; 56C715FD1F0D36C600770048 /* ContactsAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsAdapter.h; sourceTree = "<group>"; }; 56C715FE1F0D36C600770048 /* ContactsAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContactsAdapter.mm; sourceTree = "<group>"; }; - 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsAdapterDelegate.swift; sourceTree = "<group>"; }; - 56C716021F0D466100770048 /* ContactsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsService.swift; sourceTree = "<group>"; }; 5C093F001FB495830011D90E /* Differentiator.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Differentiator.framework; path = Carthage/Build/iOS/Differentiator.framework; sourceTree = "<group>"; }; 5CE66F731FBF769B00EE9291 /* InitialLoadingViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = InitialLoadingViewController.storyboard; sourceTree = "<group>"; }; 5CE66F741FBF769B00EE9291 /* InitialLoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialLoadingViewController.swift; sourceTree = "<group>"; }; @@ -778,6 +775,8 @@ 645BDD6C24B7415A009129B1 /* MessageCellLocationSharingSent.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellLocationSharingSent.xib; sourceTree = "<group>"; }; 645BDD7024B7415A009129B1 /* MessageCellLocationSharingSent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellLocationSharingSent.swift; sourceTree = "<group>"; }; 645BDD8024B74BCB009129B1 /* LocationSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingService.swift; sourceTree = "<group>"; }; + 64DBCD1E24DB3CA900CB5CA2 /* UserSearchResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserSearchResponse.h; sourceTree = "<group>"; }; + 64DBCD2124DB3CF600CB5CA2 /* UserSearchResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserSearchResponse.m; sourceTree = "<group>"; }; 64F8127324B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellLocationSharingReceived.swift; sourceTree = "<group>"; }; 64F8127624B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellLocationSharingReceived.xib; sourceTree = "<group>"; }; 64F8127924BBC19C00A7DE6A /* MessageCellLocationSharing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLocationSharing.swift; sourceTree = "<group>"; }; @@ -953,8 +952,6 @@ 02C9B63E1E1D4E8C00F82F0C /* ServiceEvent.swift */, 564C44611E943DE6000F92B1 /* NameService.swift */, 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */, - 56C716021F0D466100770048 /* ContactsService.swift */, - 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */, 1A5DC01D1F355DA70075E8EF /* ContactsAdapterDelegate.swift */, 1A5DC01F1F355DCF0075E8EF /* ContactsService.swift */, 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */, @@ -999,8 +996,6 @@ 04399AA81D1C304300E99CD9 /* DRingAdapter.mm */, 04399AAA1D1C304300E99CD9 /* Utils.h */, 04399AAB1D1C304300E99CD9 /* Utils.mm */, - 1A5DC00A1F3558980075E8EF /* ContactsAdapter.h */, - 1A5DC00D1F3559070075E8EF /* ContactsAdapter.mm */, 563AEC741EA66487003A5641 /* AccountCreation */, 563AEC731EA6627F003A5641 /* NameRegistration */, 62A88D351F6C2E5F00F8AB18 /* PresenceAdapter.h */, @@ -1514,9 +1509,9 @@ 1A5DC02F1F3565AE0075E8EF /* SmartlistViewController.swift */, 1A2D18B51F29164700B2C785 /* SmartlistViewModel.swift */, 1A5DC0311F3566140075E8EF /* ConversationSection.swift */, + 2662FC80246B793500FA7782 /* IncognitoSmartListViewController.storyboard */, 2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */, 2662FC7E246B790400FA7782 /* IncognitoSmartListViewModel.swift */, - 2662FC80246B793500FA7782 /* IncognitoSmartListViewController.storyboard */, ); path = Smartlist; sourceTree = "<group>"; @@ -1698,6 +1693,8 @@ 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */, 56308BA51EA00E5700660275 /* NameRegistrationResponse.h */, 56308BA61EA00E5700660275 /* NameRegistrationResponse.m */, + 64DBCD1E24DB3CA900CB5CA2 /* UserSearchResponse.h */, + 64DBCD2124DB3CF600CB5CA2 /* UserSearchResponse.m */, ); path = NameRegistration; sourceTree = "<group>"; @@ -2287,6 +2284,7 @@ 564C44621E943DE6000F92B1 /* NameService.swift in Sources */, 1A5DC02C1F3565250075E8EF /* MeViewController.swift in Sources */, 0EF78DE31FD0AE3000FC6966 /* ConversationsManager.swift in Sources */, + 64DBCD2224DB3CF600CB5CA2 /* UserSearchResponse.m in Sources */, 263B715A246D9556007044C4 /* IncognitoSmartListCell.swift in Sources */, 66266FC021557D2F002757A6 /* ScanViewModel.swift in Sources */, 0E5A668322F0B1F100AA6820 /* ProgressView.swift in Sources */, diff --git a/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.h b/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.h index e75d6af129c577a170a9e307d5cfb1af79f66b32..f81b70051389e4ee0461b71b96b91efe12157db8 100644 --- a/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.h +++ b/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.h @@ -1,7 +1,8 @@ /* - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. + * Copyright (C) 2017-2020 Savoir-faire Linux Inc. * * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,5 +36,6 @@ - (void)lookupAddressWithAccount:(NSString*)account nameserver:(NSString*)nameserver address:(NSString*)address; +- (void)searchUserWithAccount:(NSString*)account query:(NSString*)query; @end diff --git a/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.mm b/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.mm index 671804703326cfb281df94f116d8d20db33628a8..7ba5357b0fc62349639af0108166808665cf8df6 100644 --- a/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.mm +++ b/Ring/Ring/Bridging/NameRegistration/NameRegistrationAdapter.mm @@ -1,7 +1,8 @@ /* - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. + * Copyright (C) 2017-2020 Savoir-faire Linux Inc. * * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +25,7 @@ #import "dring/configurationmanager_interface.h" #import "LookupNameResponse.h" #import "NameRegistrationResponse.h" +#import "UserSearchResponse.h" @implementation NameRegistrationAdapter @@ -59,8 +61,8 @@ static id <NameRegistrationAdapterDelegate> _delegate; })); confHandlers.insert(exportable_callback<ConfigurationSignal::NameRegistrationEnded>([&](const std::string&account_id, - int state, - const std::string& name) { + int state, + const std::string& name) { if (NameRegistrationAdapter.delegate) { NameRegistrationResponse* response = [NameRegistrationResponse new]; response.accountId = [NSString stringWithUTF8String:account_id.c_str()]; @@ -70,6 +72,20 @@ static id <NameRegistrationAdapterDelegate> _delegate; } })); + confHandlers.insert(exportable_callback<ConfigurationSignal::UserSearchEnded>([&](const std::string&account_id, + int state, + const std::string&query, + const std::vector<std::map<std::string,std::string>>&results) { + if (NameRegistrationAdapter.delegate) { + UserSearchResponse* response = [UserSearchResponse new]; + response.accountId = [NSString stringWithUTF8String:account_id.c_str()]; + response.state = (UserSearchState)state; + response.query = [NSString stringWithUTF8String:query.c_str()]; + response.results = [Utils vectorOfMapsToArray:results]; + [NameRegistrationAdapter.delegate userSearchEndedWith:response]; + } + })); + registerSignalHandlers(confHandlers); } #pragma mark - @@ -86,6 +102,10 @@ static id <NameRegistrationAdapterDelegate> _delegate; registerName(std::string([account UTF8String]), std::string([password UTF8String]), std::string([name UTF8String])); } +- (void)searchUserWithAccount:(NSString*)account query:(NSString*)query { + searchUser(std::string([account UTF8String]), std::string([query UTF8String])); +} + #pragma mark NameRegistrationAdapterDelegate + (id <NameRegistrationAdapterDelegate>)delegate { return _delegate; diff --git a/Ring/Ring/Bridging/NameRegistration/UserSearchResponse.h b/Ring/Ring/Bridging/NameRegistration/UserSearchResponse.h new file mode 100644 index 0000000000000000000000000000000000000000..a57e09edaeec07e7d6aa3e2c33bf4802d874d7b2 --- /dev/null +++ b/Ring/Ring/Bridging/NameRegistration/UserSearchResponse.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#import <Foundation/Foundation.h> + +//Represents the status of the user search response from to the daemon +typedef NS_ENUM(NSInteger, UserSearchState) { + UserSearchStateFound = 0, + UserSearchStateInvalidName, + UserSearchStateNotFound, + UserSearchStateError +}; + +@interface UserSearchResponse : NSObject + +@property (nonatomic, retain) NSString* accountId; +@property (nonatomic) UserSearchState state; +@property (nonatomic, retain) NSString* query; +@property (nonatomic, retain) NSArray* results; + +@end diff --git a/Ring/Ring/Bridging/NameRegistration/UserSearchResponse.m b/Ring/Ring/Bridging/NameRegistration/UserSearchResponse.m new file mode 100644 index 0000000000000000000000000000000000000000..f33831dc05a876e4f708268b8b240f927ecd652b --- /dev/null +++ b/Ring/Ring/Bridging/NameRegistration/UserSearchResponse.m @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#import "UserSearchResponse.h" + +@implementation UserSearchResponse + +@end diff --git a/Ring/Ring/Bridging/Ring-Bridging-Header.h b/Ring/Ring/Bridging/Ring-Bridging-Header.h index e2316214e5a41c191c453d5a0a4adf596f0e2065..5cad15499ea494c6406cd7b1d75110ed42396215 100644 --- a/Ring/Ring/Bridging/Ring-Bridging-Header.h +++ b/Ring/Ring/Bridging/Ring-Bridging-Header.h @@ -28,6 +28,7 @@ #import "DRingAdapter.h" #import "NameRegistrationAdapter.h" #import "LookupNameResponse.h" +#import "UserSearchResponse.h" #import "RegistrationResponse.h" #import "NameRegistrationResponse.h" #import "MessagesAdapter.h" diff --git a/Ring/Ring/Database/DBManager.swift b/Ring/Ring/Database/DBManager.swift index e41357feed7e8af26a5430b7b9489057751258f6..2e1598debeb90eaf1cbca828fc84082cf6f4fd41 100644 --- a/Ring/Ring/Database/DBManager.swift +++ b/Ring/Ring/Database/DBManager.swift @@ -717,7 +717,8 @@ class DBManager { return self.interactionHepler.insert(item: interaction, dataBase: dataBase) } - func getProfile(for profileUri: String, createIfNotExists: Bool, accountId: String) throws -> Profile? { + func getProfile(for profileUri: String, createIfNotExists: Bool, accountId: String, + alias: String? = nil, photo: String? = nil) throws -> Profile? { let type = profileUri.contains("ring") ? ProfileType.ring : ProfileType.sip if createIfNotExists && type == ProfileType.sip { self.dbConnections.createAccountfolder(for: accountId) @@ -731,7 +732,7 @@ class DBManager { profileURI: profileUri) || !createIfNotExists { return getProfileFromPath(path: profilePath) } - let profile = Profile(profileUri, nil, nil, type.rawValue) + let profile = Profile(profileUri, alias, photo, type.rawValue) try self.saveProfile(profile: profile, path: profilePath) return getProfileFromPath(path: profilePath) } diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index e1a6b51e45afb24a7e4b69be5c4cc2b4218ef97a..5bf8a8fc7d5739298689027f774a5ebf5ace33f8 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -30,9 +30,7 @@ import SwiftyBeaver // swiftlint:disable file_length class ConversationViewModel: Stateable, ViewModel { - /** - logguer - */ + /// Logger private let log = SwiftyBeaver.self //Services @@ -48,16 +46,14 @@ class ConversationViewModel: Stateable, ViewModel { private let injectionBag: InjectionBag - private var players = [String: PlayerViewModel]() + private let disposeBag = DisposeBag() - func getPlayer(messageID: String) -> PlayerViewModel? { - return players[messageID] - } + var messages = Variable([MessageViewModel]()) - func setPlayer(messageID: String, player: PlayerViewModel) { - players[messageID] = player - } + private var players = [String: PlayerViewModel]() + func getPlayer(messageID: String) -> PlayerViewModel? { return players[messageID] } + func setPlayer(messageID: String, player: PlayerViewModel) { players[messageID] = player } func closeAllPlayers() { let queue = DispatchQueue.global(qos: .default) queue.sync { @@ -76,18 +72,46 @@ class ConversationViewModel: Stateable, ViewModel { lazy var typingIndicator: Observable<Bool> = { return self.conversationsService .sharedResponseStream - .filter { [weak self] (event) -> Bool in + .filter({ [weak self] (event) -> Bool in return event.eventType == ServiceEventType.messageTypingIndicator && event.getEventInput(ServiceEventInput.accountId) == self?.conversation.value.accountId && event.getEventInput(ServiceEventInput.peerUri) == self?.conversation.value.hash - }.map { (event) -> Bool in - if let status: Int = event.getEventInput(ServiceEventInput.state), status == 1 { - return true - } - return false - } + }) + .map({ (event) -> Bool in + if let status: Int = event.getEventInput(ServiceEventInput.state), status == 1 { + return true + } + return false + }) + }() + + private var contactUri: String { self.conversation.value.participantUri } + + private var isJamsAccount: Bool { self.accountService.isJams(for: self.conversation.value.accountId) } + + var isAccountSip: Bool = false + + var displayName = Variable<String?>(nil) + var userName = Variable<String>("") + lazy var bestName: Observable<String> = { + return Observable + .combineLatest(userName.asObservable(), + displayName.asObservable(), + resultSelector: {(userName, displayname) in + guard let displayname = displayname, !displayname.isEmpty else { return userName } + return displayname + }) }() + /// My contact's profile's image data + var profileImageData = Variable<Data?>(nil) + /// My profile's image data + var myOwnProfileImageData: Data? + + var inviteButtonIsAvailable = BehaviorSubject(value: true) + + var contactPresence = Variable<Bool>(false) + required init(with injectionBag: InjectionBag) { self.injectionBag = injectionBag self.accountService = injectionBag.accountService @@ -99,251 +123,104 @@ class ConversationViewModel: Stateable, ViewModel { self.dataTransferService = injectionBag.dataTransferService self.callService = injectionBag.callService self.locationSharingService = injectionBag.locationSharingService - - dateFormatter.dateStyle = .medium - hourFormatter.dateFormat = "HH:mm" - - self.subscribeLocationReceivedEvent() - self.subscribeProfileServiceEvent() } - private func subscribeLocationReceivedEvent() { - self.locationSharingService - .peerUriAndLocationReceived - .subscribe(onNext: { [weak self] tuple in - guard let self = self, let peerUri = tuple.0, let conversation = self.conversation else { return } - let coordinates = tuple.1 - if peerUri == conversation.value.participantUri { - self.myContactsLocation.onNext(coordinates) - } - }) - .disposed(by: self.disposeBag) + private func setConversation(_ conversation: ConversationModel) { + self.conversation = Variable<ConversationModel>(conversation) } - private func subscribeProfileServiceEvent() { - guard let account = self.accountService.currentAccount else { return } - self.profileService - .getAccountProfile(accountId: account.id) - .subscribe(onNext: { [weak self] profile in - guard let self = self else { return } - if let photo = profile.photo, - let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? { - self.myOwnProfileImageData = data - } - }) - .disposed(by: self.disposeBag) + convenience init(with injectionBag: InjectionBag, conversation: ConversationModel, user: JamiSearchViewModel.UserSearchModel) { + self.init(with: injectionBag) + self.userName.value = user.username + self.displayName.value = user.firstName + " " + user.lastName + self.profileImageData.value = user.profilePicture + self.setConversation(conversation) // required to trigger the didSet } var conversation: Variable<ConversationModel>! { didSet { - let contactUri = self.conversation.value.participantUri - self.conversationsService - .conversationsForCurrentAccount - .map({ [weak self] conversations in - return conversations.filter({ conv -> Bool in - let recipient1 = conv.participantUri - let recipient2 = contactUri - if recipient1 == recipient2 { - return true - } - return false - }).map({ [weak self] conversation -> (ConversationModel) in - self?.conversation.value = conversation - return conversation - }) - .flatMap({ conversation in - conversation.messages.map({ message -> MessageViewModel? in - if let injBag = self?.injectionBag { - let lastDisplayed = self?.isLastDisplayed(messageId: message.messageId) ?? false - return MessageViewModel(withInjectionBag: injBag, withMessage: message, isLastDisplayed: lastDisplayed) - } - return nil - }) - }).filter { (message) -> Bool in - message != nil - }.map { (message) -> MessageViewModel in - return message! - } - }) - .observeOn(MainScheduler.instance) - .subscribe(onNext: { [weak self] messageViewModels in - guard let self = self else { return } - var msg = messageViewModels - if self.peerComposingMessage { - let msgModel = MessageModel(withId: "", - receivedDate: Date(), - content: " ", - authorURI: self.conversation.value.participantUri, - incoming: true) - let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel, isLastDisplayed: false) - composingIndicator.isComposingIndicator = true - msg.append(composingIndicator) - } - self.messages.value = msg - }).disposed(by: self.disposeBag) - - self.contactsService - .getContactRequestVCard(forContactWithRingId: self.conversation.value.hash) - .subscribe(onSuccess: { [weak self] vCard in - guard let imageData = vCard.imageData else { - self?.log.warning("vCard for ringId: \(contactUri) has no image") - return + if self.isJamsAccount { // fixes image and displayname not showing when adding contact for first time + if let profile = self.contactsService.getProfile(uri: self.contactUri, accountId: self.conversation.value.accountId), + let alias = profile.alias, let photo = profile.photo { + self.displayName.value = alias + if let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? { + self.profileImageData.value = data } - self?.profileImageData.value = imageData - self?.displayName.value = VCardUtils.getName(from: vCard) - }) - .disposed(by: self.disposeBag) + } + } - self.profileService - .getProfile(uri: contactUri, - createIfNotexists: false, - accountId: self.conversation.value.accountId) - .subscribe(onNext: { [weak self] profile in - self?.displayName.value = profile.alias - if let photo = profile.photo, - let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? { - self?.profileImageData.value = data - } - }).disposed(by: disposeBag) + self.subscribeConversationServiceConversations() + + if !self.isJamsAccount { + self.subscribeContactServiceRequestVCard() + } + self.subscribeProfileServiceContactPhoto() + + // Used for location sharing feature + self.subscribeLocationServiceLocationReceived() + self.subscribeProfileServiceMyPhoto() - if let account = self.accountService - .getAccount(fromAccountId: self.conversation.value.accountId), + if let account = self.accountService.getAccount(fromAccountId: self.conversation.value.accountId), account.type == AccountType.sip { - self.userName.value = self.conversation.value.hash - self.isAccountSip = true - return + self.userName.value = self.conversation.value.hash + self.isAccountSip = true + return } + // invite and block buttons - let contact = self.contactsService.contact(withUri: contactUri) + let contact = self.contactsService.contact(withUri: self.contactUri) if contact != nil { self.inviteButtonIsAvailable.onNext(false) } - self.contactsService.contactStatus.filter({ cont in - return cont.uriString == contactUri - }) - .subscribe(onNext: { [weak self] _ in - self?.inviteButtonIsAvailable.onNext(false) - }).disposed(by: self.disposeBag) - - // subscribe to presence updates for the conversation's associated contact - if let contactPresence = self.presenceService - .contactPresence[self.conversation.value.hash] { - self.contactPresence = contactPresence - } else { - self.contactPresence.value = false - presenceService.sharedResponseStream - .filter({ [weak self] serviceEvent in - guard let uri: String = serviceEvent - .getEventInput(ServiceEventInput.uri), - let accountID: String = serviceEvent - .getEventInput(ServiceEventInput.accountId) else { return false } - return uri == self?.conversation.value.hash && - accountID == self?.conversation.value.accountId - }).subscribe(onNext: { [weak self] _ in - self?.subscribePresence() - }).disposed(by: self.disposeBag) - } + self.subscribeContactServiceContactStatus() - if let contactUserName = contact?.userName { - self.userName.value = contactUserName - } else { - self.userName.value = self.conversation.value.hash - // Return an observer for the username lookup - self.nameService.usernameLookupStatus - .filter({ [weak self] lookupNameResponse in - return lookupNameResponse.address != nil && - (lookupNameResponse.address == contactUri || - lookupNameResponse.address == self?.conversation.value.hash) - }).subscribe(onNext: { [weak self] lookupNameResponse in - if let name = lookupNameResponse.name, !name.isEmpty { - self?.userName.value = name - contact?.userName = name - } else if let address = lookupNameResponse.address { - self?.userName.value = address - } - }).disposed(by: disposeBag) - - self.nameService.lookupAddress(withAccount: self.conversation.value.accountId, nameserver: "", address: self.conversation.value.hash) - } - self.typingIndicator - .subscribe(onNext: { [weak self] (typing) in - if typing { - self?.addComposingIndicatorMsg() + self.subscribePresenceServiceContactPresence() + + if !self.isJamsAccount || contact != nil { + if let contactUserName = contact?.userName { + self.userName.value = contactUserName } else { - self?.removeComposingIndicatorMsg() + self.userName.value = self.conversation.value.hash + + self.subscribeUserServiceLookupStatus() + self.nameService.lookupAddress(withAccount: self.conversation.value.accountId, nameserver: "", address: self.conversation.value.hash) } - }).disposed(by: self.disposeBag) - } - } + } - func subscribePresence() { - if let contactPresence = self.presenceService - .contactPresence[self.conversation.value.hash] { - self.contactPresence = contactPresence - } else { - self.contactPresence.value = false + self.subscribeConversationServiceTypingIndicator() } } //Displays the entire date ( for messages received before the current week ) - private let dateFormatter = DateFormatter() + private lazy var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() //Displays the hour of the message reception ( for messages received today ) - private let hourFormatter = DateFormatter() - - private let disposeBag = DisposeBag() - - var messages = Variable([MessageViewModel]()) - - var displayName = Variable<String?>(nil) - - var userName = Variable<String>("") - - var isAccountSip: Bool = false - - lazy var bestName: Observable<String> = { - return Observable - .combineLatest(userName.asObservable(), - displayName.asObservable()) {(userName, displayname) in - guard let displayname = displayname, !displayname.isEmpty else { return userName } - return displayname - } + private lazy var hourFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm" + return formatter }() - // My contact's - var profileImageData = Variable<Data?>(nil) - - // Mine - var myOwnProfileImageData: Data? - - var inviteButtonIsAvailable = BehaviorSubject(value: true) - - var contactPresence = Variable<Bool>(false) - - var unreadMessages: String { - return self.unreadMessagesCount.description + private var unreadMessagesCount: Int { + let unreadMessages = self.conversation.value.messages.filter({ $0.status != .displayed && !$0.isTransfer && $0.incoming }) + return unreadMessages.count } - var hasUnreadMessages: Bool { - return unreadMessagesCount > 0 - } + var unreadMessages: String { self.unreadMessagesCount.description } - var lastMessage: String { - let messages = self.messages.value - if let lastMessage = messages.last?.content { - return lastMessage - } else { - return "" - } - } + var hasUnreadMessages: Bool { unreadMessagesCount > 0 } + + var lastMessage: String { self.messages.value.last?.content ?? "" } var lastMessageReceivedDate: String { - guard let lastMessageDate = self.conversation.value.messages.last?.receivedDate else { - return "" - } + guard let lastMessageDate = self.conversation.value.messages.last?.receivedDate else { return "" } let dateToday = Date() @@ -370,13 +247,9 @@ class ConversationViewModel: Stateable, ViewModel { } } - var hideNewMessagesLabel: Bool { - return self.unreadMessagesCount == 0 - } + var hideNewMessagesLabel: Bool { self.unreadMessagesCount == 0 } - var hideDate: Bool { - return self.conversation.value.messages.isEmpty - } + var hideDate: Bool { self.conversation.value.messages.isEmpty } func sendMessage(withContent content: String) { // send a contact request if this is the first message (implicitly not a contact) @@ -400,12 +273,8 @@ class ConversationViewModel: Stateable, ViewModel { } func setMessagesAsRead() { - guard let account = self.accountService.currentAccount else { - return - } - guard let ringId = AccountModelHelper(withAccount: account).ringId else { - return - } + guard let account = self.accountService.currentAccount, + let ringId = AccountModelHelper(withAccount: account).ringId else { return } self.conversationsService .setMessagesAsRead(forConversation: self.conversation.value, @@ -417,12 +286,9 @@ class ConversationViewModel: Stateable, ViewModel { } func setMessageAsRead(daemonId: String, messageId: Int64) { - guard let account = self.accountService.currentAccount else { - return - } - guard let accountURI = AccountModelHelper(withAccount: account).ringId else { - return - } + guard let account = self.accountService.currentAccount, + let accountURI = AccountModelHelper(withAccount: account).ringId else { return } + self.conversationsService .setMessageAsRead(daemonId: daemonId, messageID: messageId, @@ -445,23 +311,18 @@ class ConversationViewModel: Stateable, ViewModel { self.messages.value.removeAll(where: { $0.messageId == messageId }) } - private var unreadMessagesCount: Int { - let unreadMessages = self.conversation.value.messages - .filter({ message in - return message.status != .displayed && - !message.isTransfer && message.incoming - }) - return unreadMessages.count - } - func sendContactRequest() { - if let contact = self.contactsService - .contact(withUri: self.conversation.value.participantUri), - contact.banned { - return + guard let currentAccount = self.accountService.currentAccount else { return } + + if self.isJamsAccount { + _ = self.contactsService.createProfile(with: self.contactUri, + alias: self.displayName.value!, + photo: self.profileImageData.value!.base64EncodedString(), + accountId: currentAccount.id) } - guard let currentAccount = self.accountService.currentAccount else { + if let contact = self.contactsService.contact(withUri: self.conversation.value.participantUri), + contact.banned { return } @@ -472,10 +333,13 @@ class ConversationViewModel: Stateable, ViewModel { self?.log.info("contact request sent") }, onError: { [weak self] (error) in self?.log.info(error) - }).disposed(by: self.disposeBag) - self.presenceService.subscribeBuddy(withAccountId: currentAccount.id, - withUri: self.conversation.value.hash, - withFlag: true) + }) + .disposed(by: self.disposeBag) + + self.presenceService + .subscribeBuddy(withAccountId: currentAccount.id, + withUri: self.conversation.value.hash, + withFlag: true) } func block() { @@ -694,7 +558,191 @@ class ConversationViewModel: Stateable, ViewModel { var myContactsLocation = BehaviorSubject<CLLocationCoordinate2D?>(value: nil) } -// MARK: Sharing my location +// MARK: Conversation didSet functions +extension ConversationViewModel { + + private func subscribeConversationServiceConversations() { + let contactUri = self.contactUri + + self.conversationsService + .conversationsForCurrentAccount + .map({ [weak self] conversations in + return conversations + .filter({ conv -> Bool in + let recipient1 = conv.participantUri + let recipient2 = contactUri + return recipient1 == recipient2 + }) + .map({ [weak self] conversation -> (ConversationModel) in + self?.conversation.value = conversation + return conversation + }) + .flatMap({ conversation in + conversation.messages.map({ message -> MessageViewModel? in + if let injBag = self?.injectionBag { + let lastDisplayed = self?.isLastDisplayed(messageId: message.messageId) ?? false + return MessageViewModel(withInjectionBag: injBag, withMessage: message, isLastDisplayed: lastDisplayed) + } + return nil + }) + }) + .filter({ (message) -> Bool in + message != nil + }) + .map({ (message) -> MessageViewModel in + return message! + }) + }) + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self] messageViewModels in + guard let self = self else { return } + var msg = messageViewModels + if self.peerComposingMessage { + let msgModel = MessageModel(withId: "", + receivedDate: Date(), + content: " ", + authorURI: self.conversation.value.participantUri, + incoming: true) + let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel, isLastDisplayed: false) + composingIndicator.isComposingIndicator = true + msg.append(composingIndicator) + } + self.messages.value = msg + }) + .disposed(by: self.disposeBag) + } + + private func subscribeLocationServiceLocationReceived() { + self.locationSharingService + .peerUriAndLocationReceived + .subscribe(onNext: { [weak self] tuple in + guard let self = self, let peerUri = tuple.0, let conversation = self.conversation else { return } + let coordinates = tuple.1 + if peerUri == conversation.value.participantUri { + self.myContactsLocation.onNext(coordinates) + } + }) + .disposed(by: self.disposeBag) + } + + private func subscribeContactServiceRequestVCard() { + self.contactsService + .getContactRequestVCard(forContactWithRingId: self.conversation.value.hash) + .subscribe(onSuccess: { [weak self] vCard in + guard let imageData = vCard.imageData else { + self?.log.warning("vCard for ringId: \(String(describing: self?.contactUri)) has no image") + return + } + self?.profileImageData.value = imageData + self?.displayName.value = VCardUtils.getName(from: vCard) + }) + .disposed(by: self.disposeBag) + } + + private func subscribeProfileServiceContactPhoto() { + self.profileService + .getProfile(uri: self.contactUri, + createIfNotexists: false, + accountId: self.conversation.value.accountId) + .subscribe(onNext: { [weak self] profile in + self?.displayName.value = profile.alias + if let photo = profile.photo, + let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? { + self?.profileImageData.value = data + } + }) + .disposed(by: disposeBag) + } + + private func subscribeProfileServiceMyPhoto() { + guard let account = self.accountService.currentAccount else { return } + self.profileService + .getAccountProfile(accountId: account.id) + .subscribe(onNext: { [weak self] profile in + guard let self = self else { return } + if let photo = profile.photo, + let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? { + self.myOwnProfileImageData = data + } + }) + .disposed(by: self.disposeBag) + } + + private func subscribePresenceServiceContactPresence() { + // subscribe to presence updates for the conversation's associated contact + if let contactPresence = self.presenceService.contactPresence[self.conversation.value.hash] { + self.contactPresence = contactPresence + } else { + self.contactPresence.value = false + self.presenceService + .sharedResponseStream + .filter({ [weak self] serviceEvent in + guard let uri: String = serviceEvent.getEventInput(ServiceEventInput.uri), + let accountID: String = serviceEvent.getEventInput(ServiceEventInput.accountId) else { return false } + return uri == self?.conversation.value.hash && accountID == self?.conversation.value.accountId + }) + .subscribe(onNext: { [weak self] _ in + self?.subscribePresence() + }) + .disposed(by: self.disposeBag) + } + } + + private func subscribePresence() { + if let contactPresence = self.presenceService + .contactPresence[self.conversation.value.hash] { + self.contactPresence = contactPresence + } else { + self.contactPresence.value = false + } + } + + private func subscribeUserServiceLookupStatus() { + let contact = self.contactsService.contact(withUri: self.contactUri) + + // Return an observer for the username lookup + self.nameService + .usernameLookupStatus + .filter({ [weak self] lookupNameResponse in + return lookupNameResponse.address != nil && + (lookupNameResponse.address == self?.contactUri || + lookupNameResponse.address == self?.conversation.value.hash) + }) + .subscribe(onNext: { [weak self] lookupNameResponse in + if let name = lookupNameResponse.name, !name.isEmpty { + self?.userName.value = name + contact?.userName = name + } else if let address = lookupNameResponse.address { + self?.userName.value = address + } + }) + .disposed(by: disposeBag) + } + + private func subscribeConversationServiceTypingIndicator() { + self.typingIndicator + .subscribe(onNext: { [weak self] (typing) in + if typing { + self?.addComposingIndicatorMsg() + } else { + self?.removeComposingIndicatorMsg() + } + }) + .disposed(by: self.disposeBag) + } + + private func subscribeContactServiceContactStatus() { + self.contactsService + .contactStatus + .filter({ [weak self] in $0.uriString == self?.contactUri }) + .subscribe(onNext: { [weak self] _ in + self?.inviteButtonIsAvailable.onNext(false) + }) + .disposed(by: self.disposeBag) + } +} + +// MARK: Location sharing extension ConversationViewModel { func isAlreadySharingLocation() -> Bool { diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift index dde20c9b48c25b808f785dce3fac5e818fdfbec1..7787d527912d1a5445aa20570c7d069545a66a79 100644 --- a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift @@ -60,26 +60,16 @@ class ConversationCell: UITableViewCell, NibReusable { func configureFromItem(_ item: ConversationSection.Item) { // avatar Observable<(Data?, String)>.combineLatest(item.profileImageData.asObservable(), - item.userName.asObservable(), - item.displayName.asObservable()) { profileImage, username, displayName in - if let displayName = displayName, !displayName.isEmpty { - return (profileImage, displayName) - } - return (profileImage, username) - } + item.bestName.asObservable()) { ($0, $1) } .observeOn(MainScheduler.instance) .startWith((item.profileImageData.value, item.userName.value)) - .subscribe({ [weak self] profileData -> Void in - guard let data = profileData.element?.1 else { - return - } + .subscribe({ [weak self] profileData in + guard let data = profileData.element?.1 else { return } + self?.avatarView.subviews.forEach({ $0.removeFromSuperview() }) - self?.avatarView - .addSubview( - AvatarView(profileImageData: profileData.element?.0, - username: data, - size: self?.avatarSize ?? 40)) - return + self?.avatarView.addSubview(AvatarView(profileImageData: profileData.element?.0, + username: data, + size: self?.avatarSize ?? 40)) }) .disposed(by: self.disposeBag) @@ -89,12 +79,12 @@ class ConversationCell: UITableViewCell, NibReusable { // presence if self.presenceIndicator != nil { - item.contactPresence.asObservable() - .observeOn(MainScheduler.instance) - .map { value in !value } - .bind(to: self.presenceIndicator!.rx.isHidden) - .disposed(by: self.disposeBag) - } + item.contactPresence.asObservable() + .observeOn(MainScheduler.instance) + .map { value in !value } + .bind(to: self.presenceIndicator!.rx.isHidden) + .disposed(by: self.disposeBag) + } // username item.bestName.asObservable() diff --git a/Ring/Ring/Features/Conversations/SmartList/ConversationSection.swift b/Ring/Ring/Features/Conversations/SmartList/ConversationSection.swift index 0fac99be4ddb32ee44f920a49deb67d46975d9fd..2484a4d51ee5a7d1dfe5379b9f35ec42e9ff299c 100644 --- a/Ring/Ring/Features/Conversations/SmartList/ConversationSection.swift +++ b/Ring/Ring/Features/Conversations/SmartList/ConversationSection.swift @@ -22,7 +22,7 @@ import RxDataSources struct ConversationSection { var header: String - var items: [ConversationViewModel] + var items: [Item] } extension ConversationSection: SectionModelType { diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift index 746d2604a02056a97c1fc53c668f2b3279d42ec1..ab15c1fed9cdc35eabac76e86663fe1398bbbb9a 100644 --- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift @@ -46,9 +46,7 @@ class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { private let profileService: ProfilesService private let callService: CallsService - lazy var currentAccount: AccountModel? = { - return self.accountsService.currentAccount - }() + var currentAccount: AccountModel? { self.accountsService.currentAccount } var searching = PublishSubject<Bool>() @@ -57,8 +55,7 @@ class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { lazy var hideNoConversationsMessage: Observable<Bool> = { return Observable<Bool> .combineLatest(self.conversations, - self.searching.asObservable() - .startWith(false), + self.searching.asObservable().startWith(false), resultSelector: {(conversations, searching) -> Bool in if searching { return true } if let convf = conversations.first { @@ -73,16 +70,17 @@ class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { return self.accountsService .accountsObservable.asObservable() .map({ [weak self] accountsModels in - var items = [AccountItem]() - guard let self = self else { return items } - for account in accountsModels { - items.append(AccountItem(account: account, - profileObservable: self.profileService.getAccountProfile(accountId: account.id))) - } - return items - }) + var items = [AccountItem]() + guard let self = self else { return items } + for account in accountsModels { + items.append(AccountItem(account: account, + profileObservable: self.profileService.getAccountProfile(accountId: account.id))) + } + return items + }) }() + /// For FilterConversationDataSource protocol var conversationViewModels = [ConversationViewModel]() func networkConnectionState() -> ConnectionType { @@ -221,6 +219,7 @@ class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { .disposed(by: self.disposeBag) } + /// For FilterConversationDataSource protocol func conversationFound(conversation: ConversationViewModel?, name: String) { contactFoundConversation.value = conversation } @@ -273,7 +272,8 @@ class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { } } - func showConversation (withConversationViewModel conversationViewModel: ConversationViewModel) { + /// For FilterConversationDataSource protocol + func showConversation(withConversationViewModel conversationViewModel: ConversationViewModel) { self.stateSubject.onNext(ConversationState.conversationDetail(conversationViewModel: conversationViewModel)) } diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift index 00b9aac65b78e3b5fb5d285f16c10763f0bd32a0..b26571e8a8433057e9f55008184abfe3f4652bf1 100644 --- a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift +++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift @@ -2,6 +2,7 @@ * Copyright (C) 2020 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> +* Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,7 +25,8 @@ import RxDataSources import RxCocoa import Reusable -class JamiSearchView: NSObject, UITableViewDelegate { +class JamiSearchView: NSObject { + @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var searchingLabel: UILabel! @IBOutlet weak var searchResultsTableView: UITableView! @@ -38,9 +40,9 @@ class JamiSearchView: NSObject, UITableViewDelegate { let incognitoHeaderHeight: CGFloat = 0 func configure(with injectionBag: InjectionBag, source: FilterConversationDataSource, isIncognito: Bool) { - viewModel = JamiSearchViewModel(with: injectionBag, source: source) + self.viewModel = JamiSearchViewModel(with: injectionBag, source: source) self.isIncognito = isIncognito - setUpView() + self.setUpView() } private func setUpView() { @@ -54,43 +56,49 @@ class JamiSearchView: NSObject, UITableViewDelegate { } private func configureSearchResult() { - let cellType = isIncognito ? IncognitoSmartListCell.self : SmartListCell.self - searchResultsTableView.register(cellType: cellType) - - searchResultsTableView.rowHeight = isIncognito ? incognitoCellHeight : SmartlistConstants.smartlistRowHeight - searchResultsTableView.backgroundColor = UIColor.jamiBackgroundColor - if !isIncognito { - searchResultsTableView.tableFooterView = UIView() + if self.isIncognito { + self.searchResultsTableView.register(cellType: IncognitoSmartListCell.self) + self.searchResultsTableView.rowHeight = self.incognitoCellHeight + + } else { + self.searchResultsTableView.register(cellType: SmartListCell.self) + self.searchResultsTableView.rowHeight = SmartlistConstants.smartlistRowHeight + self.searchResultsTableView.tableFooterView = UIView() } - searchResultsTableView.rx.setDelegate(self).disposed(by: disposeBag) - let configureCell: (TableViewSectionedDataSource, UITableView, IndexPath, ConversationSection.Item) - -> UITableViewCell = { - ( dataSource: TableViewSectionedDataSource<ConversationSection>, - tableView: UITableView, - indexPath: IndexPath, - conversationItem: ConversationSection.Item) in - - let cell = self.isIncognito ? - tableView.dequeueReusableCell(for: indexPath, - cellType: IncognitoSmartListCell.self) : - tableView.dequeueReusableCell(for: indexPath, - cellType: SmartListCell.self) - cell.configureFromItem(conversationItem) - return cell + self.searchResultsTableView.backgroundColor = UIColor.jamiBackgroundColor + + self.searchResultsTableView.rx.setDelegate(self).disposed(by: disposeBag) + + let configureCell: (TableViewSectionedDataSource, UITableView, IndexPath, ConversationSection.Item) -> UITableViewCell = { + (dataSource: TableViewSectionedDataSource<ConversationSection>, + tableView: UITableView, + indexPath: IndexPath, + conversationItem: ConversationSection.Item) in + + let cellType = self.isIncognito ? IncognitoSmartListCell.self : SmartListCell.self + let cell = tableView.dequeueReusableCell(for: indexPath, cellType: cellType) + cell.configureFromItem(conversationItem) + return cell } let searchResultsDatasource = RxTableViewSectionedReloadDataSource<ConversationSection>(configureCell: configureCell) - viewModel.searchResults.map { (conversations) -> Bool in - return conversations.isEmpty - }.subscribe(onNext: { [weak self] (hideFooterView) in - self?.searchResultsTableView.tableFooterView?.isHidden = hideFooterView - }).disposed(by: disposeBag) - self.viewModel.searchResults + self.viewModel + .searchResults + .map({ (conversations) -> Bool in return conversations.isEmpty }) + .subscribe(onNext: { [weak self] (hideFooterView) in + self?.searchResultsTableView.tableFooterView?.isHidden = hideFooterView }) + .disposed(by: disposeBag) + + self.viewModel + .searchResults .bind(to: self.searchResultsTableView.rx.items(dataSource: searchResultsDatasource)) .disposed(by: disposeBag) - searchResultsTableView.rx.itemSelected.subscribe(onNext: { [weak self] indexPath in - self?.searchResultsTableView.deselectRow(at: indexPath, animated: true) - }).disposed(by: disposeBag) + + self.searchResultsTableView.rx.itemSelected + .subscribe(onNext: { [weak self] indexPath in + self?.searchResultsTableView.deselectRow(at: indexPath, animated: true) }) + .disposed(by: disposeBag) + searchResultsDatasource.titleForHeaderInSection = { dataSource, index in return dataSource.sectionModels[index].header } @@ -101,10 +109,18 @@ class JamiSearchView: NSObject, UITableViewDelegate { .disposed(by: disposeBag) searchingLabel.textColor = UIColor.jamiLabelColor - self.viewModel.isSearching.subscribe(onNext: { [weak self] (isSearching) in - self?.searchResultsTableView.isHidden = !isSearching - self?.searchingLabel.isHidden = !isSearching - }).disposed(by: disposeBag) + self.viewModel.isSearching + .subscribe(onNext: { [weak self] (isSearching) in + self?.searchResultsTableView.isHidden = !isSearching + self?.searchingLabel.isHidden = !isSearching + }) + .disposed(by: disposeBag) + + self.viewModel.searchStatus + .subscribe(onNext: { [weak self] (searchText) in + self?.searchResultsTableView.contentInset.top = searchText.isEmpty ? 0 : 24 + }) + .disposed(by: disposeBag) } private func configureSearchBar() { @@ -152,8 +168,10 @@ class JamiSearchView: NSObject, UITableViewDelegate { searchBar.placeholder = L10n.Smartlist.searchBarPlaceholder searchBar.backgroundColor = UIColor.clear } +} // MARK: UITableViewDelegate +extension JamiSearchView: UITableViewDelegate { func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { guard let headerView = view as? UITableViewHeaderFooterView else { return } diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift index e6b22fbf41ee9d823c6c26528313deb4f73fe5a8..0d0c95140482655cc2a2c8d38bd6a8cc92551358 100644 --- a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift +++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift @@ -2,6 +2,7 @@ * Copyright (C) 2020 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> +* Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +22,7 @@ import Foundation import RxSwift import RxCocoa +import SwiftyBeaver protocol FilterConversationDataSource { var conversationViewModels: [ConversationViewModel] { get set } @@ -31,6 +33,10 @@ protocol FilterConversationDataSource { class JamiSearchViewModel { + typealias UserSearchModel = (username: String, firstName: String, lastName: String, organization: String, jamiId: String, profilePicture: Data?) + + let log = SwiftyBeaver.self + //Services private let nameService: NameService private let accountsService: AccountsService @@ -40,15 +46,24 @@ class JamiSearchViewModel { lazy var searchResults: Observable<[ConversationSection]> = { return Observable<[ConversationSection]> - .combineLatest(self.contactFoundConversation - .asObservable(), + .combineLatest(self.contactFoundConversation.asObservable(), self.filteredResults.asObservable(), - resultSelector: { contactFoundConversation, filteredResults in + self.jamsResults.asObservable(), + resultSelector: { contactFoundConversation, filteredResults, jamsResults in var sections = [ConversationSection]() + let jamsResults = JamiSearchViewModel.removeFilteredConversations(from: jamsResults, + with: filteredResults) + if !jamsResults.isEmpty { + sections.append(ConversationSection(header: L10n.Smartlist.results, items: jamsResults)) + } else if contactFoundConversation != nil { + let contactFoundConversation = JamiSearchViewModel.removeFilteredConversations(from: [contactFoundConversation!], + with: filteredResults) + if !contactFoundConversation.isEmpty { + sections.append(ConversationSection(header: L10n.Smartlist.results, items: contactFoundConversation)) + } + } if !filteredResults.isEmpty { sections.append(ConversationSection(header: L10n.Smartlist.conversations, items: filteredResults)) - } else if contactFoundConversation != nil { - sections.append(ConversationSection(header: L10n.Smartlist.results, items: [contactFoundConversation!])) } return sections }).observeOn(MainScheduler.instance) @@ -56,6 +71,7 @@ class JamiSearchViewModel { private var contactFoundConversation = BehaviorRelay<ConversationViewModel?>(value: nil) private var filteredResults = Variable([ConversationViewModel]()) + private let jamsResults = BehaviorRelay<[ConversationViewModel]>(value: []) let searchBarText = Variable<String>("") var isSearching: Observable<Bool>! @@ -66,7 +82,7 @@ class JamiSearchViewModel { self.nameService = injectionBag.nameService self.accountsService = injectionBag.accountService self.injectionBag = injectionBag - dataSource = source + self.dataSource = source //Observes if the user is searching self.isSearching = searchBarText.asObservable() @@ -85,46 +101,94 @@ class JamiSearchViewModel { //Observe username lookup self.nameService.usernameLookupStatus .observeOn(MainScheduler.instance) - .subscribe(onNext: { [unowned self, unowned injectionBag] usernameLookupStatus in - if usernameLookupStatus.state == .found && - (usernameLookupStatus.name == self.searchBarText.value - || usernameLookupStatus.address == self.searchBarText.value) { - if let conversation = self.dataSource.conversationViewModels.filter({ conversationViewModel in - conversationViewModel.conversation.value.participantUri == usernameLookupStatus.address || conversationViewModel.conversation.value.hash == usernameLookupStatus.address - }).first { + .subscribe(onNext: { [unowned self, unowned injectionBag] lookupResponse in + if lookupResponse.state == .found && (lookupResponse.name == self.searchBarText.value || lookupResponse.address == self.searchBarText.value) { + if let conversation = self.dataSource.conversationViewModels + .filter({ conversationViewModel in + conversationViewModel.conversation.value.participantUri == lookupResponse.address || + conversationViewModel.conversation.value.hash == lookupResponse.address }).first { self.contactFoundConversation.accept(conversation) self.dataSource.conversationFound(conversation: conversation, name: self.searchBarText.value) - } else { - if self.contactFoundConversation.value?.conversation.value - .participantUri != usernameLookupStatus.address && self.contactFoundConversation.value?.conversation.value - .hash != usernameLookupStatus.address { - if let account = self.accountsService.currentAccount { - let uri = JamiURI.init(schema: URIType.ring, infoHach: usernameLookupStatus.address) - //Create new converation - let conversation = ConversationModel(withParticipantUri: uri, accountId: account.id) - let newConversation = ConversationViewModel(with: injectionBag) - newConversation.conversation = Variable<ConversationModel>(conversation) - self.contactFoundConversation.accept(newConversation) - self.dataSource.conversationFound(conversation: newConversation, name: self.searchBarText.value) - } - } + + } else if self.contactFoundConversation.value?.conversation.value.participantUri != lookupResponse.address && + self.contactFoundConversation.value?.conversation.value.hash != lookupResponse.address, + let account = self.accountsService.currentAccount { + + let uri = JamiURI.init(schema: URIType.ring, infoHach: lookupResponse.address) + //Create new converation + let conversation = ConversationModel(withParticipantUri: uri, accountId: account.id) + let newConversation = ConversationViewModel(with: injectionBag) + newConversation.conversation = Variable<ConversationModel>(conversation) + self.contactFoundConversation.accept(newConversation) + self.dataSource.conversationFound(conversation: newConversation, name: self.searchBarText.value) } self.searchStatus.onNext("") } else { - if self.filteredResults.value.isEmpty - && self.contactFoundConversation.value == nil { + if self.filteredResults.value.isEmpty && self.contactFoundConversation.value == nil { self.searchStatus.onNext(L10n.Smartlist.noResults) } else { self.searchStatus.onNext("") } } }).disposed(by: disposeBag) + + self.nameService + .userSearchResponseShared + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self] nameSearchResponse in + guard let self = self, + let results = nameSearchResponse.results as? [[String: String]], + let account = self.accountsService.currentAccount else { return } + + let deserializeUser = { (dictionary: [String: String]) -> UserSearchModel? in + guard let username = dictionary["username"], let firstName = dictionary["firstName"], + let lastName = dictionary["lastName"], let organization = dictionary["organization"], + let jamiId = dictionary["id"] ?? dictionary["jamiId"], let base64Encoded = dictionary["profilePicture"] + else { return nil } + + return UserSearchModel(username: username, firstName: firstName, + lastName: lastName, organization: organization, jamiId: jamiId, + profilePicture: NSData(base64Encoded: base64Encoded, + options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data?) + } + + var newConversations: [ConversationViewModel] = [] + for result in results { + if let user = deserializeUser(result) { + + let uri = JamiURI.init(schema: URIType.ring, infoHach: user.jamiId) + let newConversation = ConversationViewModel(with: injectionBag, + conversation: ConversationModel(withParticipantUri: uri, accountId: account.id), + user: user) + newConversations.append(newConversation) + } + } + self.jamsResults.accept(newConversations) + + if self.filteredResults.value.isEmpty && self.jamsResults.value.isEmpty { + self.searchStatus.onNext(L10n.Smartlist.noResults) + } else { + self.searchStatus.onNext("") + } + }).disposed(by: self.disposeBag) + } + + static func removeFilteredConversations(from conversationViewModels: [ConversationViewModel], + with filteredResults: [ConversationViewModel]) -> [ConversationViewModel] { + return conversationViewModels + .filter({ [filteredResults] found -> Bool in + return filteredResults + .first(where: { (filtered) -> Bool in + found.conversation.value.participantUri == filtered.conversation.value.participantUri + }) == nil + }) } private func search(withText text: String) { guard let currentAccount = self.accountsService.currentAccount else { return } self.contactFoundConversation.accept(nil) + self.jamsResults.accept([]) self.dataSource.conversationFound(conversation: nil, name: "") self.filteredResults.value.removeAll() self.searchStatus.onNext("") @@ -132,16 +196,26 @@ class JamiSearchViewModel { if text.isEmpty { return } //Filter conversations - let filteredConversations = self.dataSource.conversationViewModels - .filter({conversationViewModel in - conversationViewModel.conversation.value.participantUri == text - || conversationViewModel.conversation.value.hash == text - }) + let filteredConversations = + self.dataSource.conversationViewModels + .filter({conversationViewModel in + conversationViewModel.conversation.value.accountId == currentAccount.id && + (conversationViewModel.conversation.value.participantUri == text || + conversationViewModel.conversation.value.hash == text || + conversationViewModel.userName.value.capitalized.contains(text.capitalized) || + (conversationViewModel.displayName.value ?? "").capitalized.contains(text.capitalized)) + }) if !filteredConversations.isEmpty { self.filteredResults.value = filteredConversations } + if self.accountsService.isJams(for: currentAccount.id) { + self.nameService.searchUser(withAccount: currentAccount.id, query: text) + self.searchStatus.onNext(L10n.Smartlist.searching) + return + } + if currentAccount.type == AccountType.sip { let uri = JamiURI.init(schema: URIType.sip, infoHach: text, account: currentAccount) let conversation = ConversationModel(withParticipantUri: uri, diff --git a/Ring/Ring/Features/Me/Me/MeViewModel.swift b/Ring/Ring/Features/Me/Me/MeViewModel.swift index 5a0ed59465d55acf45310808ee8753168dc14093..f551049fc4b2d82a4ad57111a203a32a526223e1 100644 --- a/Ring/Ring/Features/Me/Me/MeViewModel.swift +++ b/Ring/Ring/Features/Me/Me/MeViewModel.swift @@ -211,15 +211,26 @@ class MeViewModel: ViewModel, Stateable { }() lazy var otherJamiSettings: Observable<SettingsSection> = { - return Observable - .just(SettingsSection.accountSettings( items: [.sectionHeader(title: L10n.AccountPage.other), - .peerDiscovery, - .blockedList, - .accountState(state: self.accountStatus), - .enableAccount, - .changePassword, - .boothMode, - .removeAccount])) + let items: [SettingsSection.SectionRow] = [.sectionHeader(title: L10n.AccountPage.other), + .peerDiscovery, + .blockedList, + .accountState(state: self.accountStatus), + .enableAccount, + .changePassword, + .boothMode, + .removeAccount] + + return Observable.combineLatest(Observable.just(items), + self.accountService.currentAccountChanged.asObservable().startWith(nil), + resultSelector: { (items, _) in + var items = items + if let currentAccount = self.accountService.currentAccount, + self.accountService.isJams(for: currentAccount.id) { + items.remove(at: items.count - 2) //remove .boothMode + items.remove(at: items.count - 2) //remove .changePassword + } + return SettingsSection.accountSettings(items: items) + }) }() func hasPassword() -> Bool { diff --git a/Ring/Ring/Services/AccountsService.swift b/Ring/Ring/Services/AccountsService.swift index d51d970b2e68dd4376ba1f43ef9605079d764430..9ffba73464a5c03377a5c9389a5b79b82c736d87 100644 --- a/Ring/Ring/Services/AccountsService.swift +++ b/Ring/Ring/Services/AccountsService.swift @@ -341,7 +341,7 @@ class AccountsService: AccountAdapterDelegate { //~ Filter the daemon signals to isolate the "account created" one. let filteredDaemonSignals = self.sharedResponseStream - .filter { (serviceEvent) -> Bool in + .filter({ (serviceEvent) -> Bool in if serviceEvent.getEventInput(ServiceEventInput.accountId) != newAccountId { return false } if serviceEvent.getEventInput(ServiceEventInput.registrationState) == ErrorGeneric { throw AccountCreationError.generic @@ -352,7 +352,7 @@ class AccountsService: AccountAdapterDelegate { let isRegistered = serviceEvent.getEventInput(ServiceEventInput.registrationState) == Registered let notRegistered = serviceEvent.getEventInput(ServiceEventInput.registrationState) == Unregistered return isRegistrationStateChanged && (isRegistered || notRegistered) - } + }) //~ Make sure that we have the correct account added in the daemon, and return it. return Observable @@ -934,6 +934,11 @@ class AccountsService: AccountAdapterDelegate { return false } + func isJams(for accountId: String) -> Bool { + let accountDetails = self.getAccountDetails(fromAccountId: accountId) + return !accountDetails.get(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.managerUri)).isEmpty + } + func enableAccount(enable: Bool, accountId: String) { self.switchAccountPropertyTo(state: enable, accountId: accountId, property: ConfigKeyModel(withKey: ConfigKey.accountEnable)) } diff --git a/Ring/Ring/Services/ContactsService.swift b/Ring/Ring/Services/ContactsService.swift index 2694f75c4ec03f73227792fb0d2cd21dcbeac00b..97c964ba94874d1721fa7c27c6e6e702f0fc2bf8 100644 --- a/Ring/Ring/Services/ContactsService.swift +++ b/Ring/Ring/Services/ContactsService.swift @@ -383,6 +383,14 @@ extension ContactsService: ContactsAdapterDelegate { } } + func createProfile(with contactUri: String, alias: String, photo: String, accountId: String) -> Profile? { + do { + return try self.dbManager.getProfile(for: contactUri, createIfNotExists: true, accountId: accountId, alias: alias, photo: photo) + } catch { + return nil + } + } + func removeAllContacts(for accountId: String) { DispatchQueue.global(qos: .background).async { for contact in self.contacts.value { diff --git a/Ring/Ring/Services/NameRegistrationAdapterDelegate.swift b/Ring/Ring/Services/NameRegistrationAdapterDelegate.swift index 664acceb089d73a09d8ccbdf32b41723f725e59e..460abf36465160d532b2339e547fa01d518eb21e 100644 --- a/Ring/Ring/Services/NameRegistrationAdapterDelegate.swift +++ b/Ring/Ring/Services/NameRegistrationAdapterDelegate.swift @@ -1,7 +1,8 @@ /* - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. + * Copyright (C) 2017-2020 Savoir-faire Linux Inc. * * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,4 +22,5 @@ @objc protocol NameRegistrationAdapterDelegate { func registeredNameFound(with response: LookupNameResponse) func nameRegistrationEnded(with response: NameRegistrationResponse) + func userSearchEnded(with response: UserSearchResponse) } diff --git a/Ring/Ring/Services/NameService.swift b/Ring/Ring/Services/NameService.swift index a16b0ad69a336c646b94933ea690e339fbe1fad8..a295af50d963e9266cc5da7fa98fb2bff8d019be 100644 --- a/Ring/Ring/Services/NameService.swift +++ b/Ring/Ring/Services/NameService.swift @@ -1,7 +1,8 @@ /* - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. + * Copyright (C) 2017-2020 Savoir-faire Linux Inc. * * Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com> + * Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,42 +35,43 @@ enum UsernameValidationStatus { let registeredNamesKey = "REGISTERED_NAMES_KEY" -class NameService: NameRegistrationAdapterDelegate { - /** - logguer - */ +class NameService { + + /// Logger private let log = SwiftyBeaver.self - /** - Used to make lookup name request to the daemon - */ + private let disposeBag = DisposeBag() + + /// Used to make lookup name request to the daemon private let nameRegistrationAdapter: NameRegistrationAdapter private var delayedLookupNameCall: DispatchWorkItem? private let lookupNameCallDelay = 0.5 - /** - Status of the current username validation request - */ + /// Status of the current username validation request var usernameValidationStatus = PublishSubject<UsernameValidationStatus>() private let registrationStatus = PublishSubject<ServiceEvent>() var sharedRegistrationStatus: Observable<ServiceEvent> + /// Status of the current username lookup request + var usernameLookupStatus = PublishSubject<LookupNameResponse>() + + private let userSearchResponseStream = PublishSubject<UserSearchResponse>() + /// Triggered when we receive a UserSearchResponse from the daemon + let userSearchResponseShared: Observable<UserSearchResponse> + init(withNameRegistrationAdapter nameRegistrationAdapter: NameRegistrationAdapter) { self.nameRegistrationAdapter = nameRegistrationAdapter self.sharedRegistrationStatus = registrationStatus.share() + + self.userSearchResponseStream.disposed(by: self.disposeBag) + self.userSearchResponseShared = self.userSearchResponseStream.share() + NameRegistrationAdapter.delegate = self } - /** - Status of the current username lookup request - */ - var usernameLookupStatus = PublishSubject<LookupNameResponse>() - - /** - Make a username lookup request to the daemon - */ + /// Make a username lookup request to the daemon func lookupName(withAccount account: String, nameserver: String, name: String) { //Cancel previous lookups... @@ -92,16 +94,12 @@ class NameService: NameRegistrationAdapterDelegate { } } - /** - Make an address lookup request to the daemon - */ + /// Make an address lookup request to the daemon func lookupAddress(withAccount account: String, nameserver: String, address: String) { self.nameRegistrationAdapter.lookupAddress(withAccount: account, nameserver: nameserver, address: address) } - /** - Register the username into the the blockchain - */ + /// Register the username into the the blockchain func registerName(withAccount account: String, password: String, name: String) { self.nameRegistrationAdapter.registerName(withAccount: account, password: password, name: name) } @@ -122,13 +120,13 @@ class NameService: NameRegistrationAdapterDelegate { }) let filteredDaemonSignals = self.sharedRegistrationStatus - .filter { (serviceEvent) -> Bool in + .filter({ (serviceEvent) -> Bool in if serviceEvent.getEventInput(ServiceEventInput.accountId) != account { return false } if serviceEvent.eventType != .nameRegistrationEnded { return false } return true - } + }) return Observable .combineLatest(registerName.asObservable(), filteredDaemonSignals.asObservable()) { (_, serviceEvent) -> Bool in guard let status: NameRegistrationState = serviceEvent.getEventInput(ServiceEventInput.state) @@ -142,7 +140,14 @@ class NameService: NameRegistrationAdapterDelegate { } } - // MARK: NameService delegate + /// Make a user search request to the daemon + func searchUser(withAccount account: String, query: String) { + self.nameRegistrationAdapter.searchUser(withAccount: account, query: query) + } +} + +// MARK: NameRegistrationAdapterDelegate +extension NameService: NameRegistrationAdapterDelegate { internal func registeredNameFound(with response: LookupNameResponse) { @@ -176,4 +181,22 @@ class NameService: NameRegistrationAdapterDelegate { event.addEventInput(.accountId, value: response.accountId) self.registrationStatus.onNext(event) } + + internal func userSearchEnded(with response: UserSearchResponse) { + switch response.state { + case .found: + self.userSearchResponseStream.onNext(response) + case .invalidName: + self.userSearchResponseStream.onNext(response) + self.log.warning("User search invalid name") + case.notFound: + self.userSearchResponseStream.onNext(response) + self.log.warning("User search not found") + case .error: + self.userSearchResponseStream.onNext(response) + self.log.error("User search error") + @unknown default: + self.log.error("User search unknown default") + } + } }