diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Integration.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Integration.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..a1d66e16dd7d57f3d7fabe8cddde991cb42eda05 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Integration.xcscheme @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1340" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "Integration" + BuildableName = "Integration" + BlueprintName = "Integration" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "Integration" + BuildableName = "Integration" + BlueprintName = "Integration" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/App/client-ios/Resources/GoogleService-Info.plist b/App/client-ios/Resources/GoogleService-Info.plist index 03e09469daae0502a5202f6e63aca9db140d1d77..676030ed57400f2bf5a72dc61d4ba5ed3e5263cb 100644 --- a/App/client-ios/Resources/GoogleService-Info.plist +++ b/App/client-ios/Resources/GoogleService-Info.plist @@ -1,36 +1,36 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> -<dict> - <key>CLIENT_ID</key> - <string></string> - <key>REVERSED_CLIENT_ID</key> - <string></string> - <key>ANDROID_CLIENT_ID</key> - <string></string> - <key>API_KEY</key> - <string></string> - <key>GCM_SENDER_ID</key> - <string></string> - <key>PLIST_VERSION</key> - <string></string> - <key>BUNDLE_ID</key> - <string></string> - <key>PROJECT_ID</key> - <string></string> - <key>STORAGE_BUCKET</key> - <string></string> - <key>IS_ADS_ENABLED</key> - <false/> - <key>IS_ANALYTICS_ENABLED</key> - <false/> - <key>IS_APPINVITE_ENABLED</key> - <false/> - <key>IS_GCM_ENABLED</key> - <false/> - <key>IS_SIGNIN_ENABLED</key> - <false/> - <key>GOOGLE_APP_ID</key> - <string></string> -</dict> + <dict> + <key>CLIENT_ID</key> + <string>662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de.apps.googleusercontent.com</string> + <key>REVERSED_CLIENT_ID</key> + <string>com.googleusercontent.apps.662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de</string> + <key>ANDROID_CLIENT_ID</key> + <string>662236151640-2ughgo2dvc59dm4o39b45lbdungp2mct.apps.googleusercontent.com</string> + <key>API_KEY</key> + <string>AIzaSyCbI2yQ7pbuVSRvraqanjGcS9CDrjD7lNU</string> + <key>GCM_SENDER_ID</key> + <string>662236151640</string> + <key>PLIST_VERSION</key> + <string>1</string> + <key>BUNDLE_ID</key> + <string>io.xxlabs.messenger</string> + <key>PROJECT_ID</key> + <string>xx-messenger-6e03e</string> + <key>STORAGE_BUCKET</key> + <string>xx-messenger-6e03e.appspot.com</string> + <key>IS_ADS_ENABLED</key> + <false></false> + <key>IS_ANALYTICS_ENABLED</key> + <false></false> + <key>IS_APPINVITE_ENABLED</key> + <true></true> + <key>IS_GCM_ENABLED</key> + <true></true> + <key>IS_SIGNIN_ENABLED</key> + <true></true> + <key>GOOGLE_APP_ID</key> + <string>1:662236151640:ios:24badb58ab07515d8cef2d</string> + </dict> </plist> diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index ecb65d1f54eb3b05cda31850eb98431c6ec2d31f..11ae32299ca8629cd0e99d3bc518541a596cc26e 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -1,6 +1,7 @@ import UIKit import BackgroundTasks +import XXModels import Theme import XXLogger import Defaults @@ -91,7 +92,11 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { guard UIApplication.shared.backgroundTimeRemaining > 9 else { if !self.forceFailedPendingMessages { self.forceFailedPendingMessages = true - session.forceFailMessages() + + // TODO: We need a Message.Assignment for status +// let query = Message.Query(status: [.sending]) +// let assignment = Message.Assignments(status: .sendingFailed) +// _ = try? session.dbManager.bulkUpdateMessages(query, assignment) } return diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index b342330e9012ba1a245d39cbd26f47602c074db4..9102274af7c0c37ce986b8aba6b2607a33cd66ca 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -259,7 +259,7 @@ extension PushRouter { } case .contactChat(id: let id): if let session = try? DependencyInjection.Container.shared.resolve() as SessionType, - let contact = session.getContactWith(userId: id) { + let contact = try? session.dbManager.fetchContacts(.init(id: [id])).first { navigationController.setViewControllers([ ChatListController(), SingleChatController(contact) @@ -267,7 +267,7 @@ extension PushRouter { } case .groupChat(id: let id): if let session = try? DependencyInjection.Container.shared.resolve() as SessionType, - let info = session.getGroupChatInfoWith(groupId: id) { + let info = try? session.dbManager.fetchGroupInfos(.init(groupId: id)).first { navigationController.setViewControllers([ ChatListController(), GroupChatController(info) diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index 6299ecb676646e6078e4e4a8bf282caa174a0362..f1e5c2fe06d45ccab6ea77b5422a5df83cec9262 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -33,16 +33,16 @@ public final class GroupChatController: UIViewController { private let layoutDelegate = LayoutDelegate() private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() - private var sections = [ArraySection<ChatSection, ChatItem>]() + private var sections = [ArraySection<ChatSection, Message>]() private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>() public override var canBecomeFirstResponder: Bool { true } public override var inputAccessoryView: UIView? { inputComponent } - public init(_ info: GroupChatInfo) { + public init(_ info: GroupInfo) { let viewModel = GroupChatViewModel(info) self.viewModel = viewModel - self.members = .init(with: []) + self.members = .init(with: info.members) self.inputComponent = ChatInputView(store: .init( initialState: .init(canAddAttachments: false), @@ -60,7 +60,14 @@ public final class GroupChatController: UIViewController { super.init(nibName: nil, bundle: nil) -// header.setup(title: info.group.name, memberList: info.members.map { ($0.username, $0.photo) }) + let memberList = info.members.map { + Member( + title: ($0.nickname ?? $0.username) ?? "Fetching username...", + photo: $0.photo + ) + } + + header.setup(title: info.group.name, memberList: memberList) } public required init?(coder: NSCoder) { nil } @@ -155,7 +162,9 @@ public final class GroupChatController: UIViewController { viewModel.replyPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in inputComponent.setupReply(message: $0.text, sender: $0.sender) } + .sink { [unowned self] senderTitle, messageText in + inputComponent.setupReply(message: messageText, sender: senderTitle) + } .store(in: &cancellables) } @@ -328,7 +337,7 @@ extension GroupChatController: UICollectionViewDataSource { let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) if item.status == .received { - if item.payload.reply != nil { + if item.replyMessageId != nil { let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) // Bubbler.buildReplyGroup( @@ -356,7 +365,7 @@ extension GroupChatController: UICollectionViewDataSource { return cell } } else if item.status == .sendingFailed { - if item.payload.reply != nil { + if item.replyMessageId != nil { let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) // Bubbler.buildReplyGroup( @@ -383,7 +392,7 @@ extension GroupChatController: UICollectionViewDataSource { return cell } } else { - if item.payload.reply != nil { + if item.replyMessageId != nil { let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) // Bubbler.buildReplyGroup( @@ -524,7 +533,7 @@ extension GroupChatController: UICollectionViewDelegate { let item = self.sections[indexPath.section].elements[indexPath.item] let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in - UIPasteboard.general.string = item.payload.text + UIPasteboard.general.string = item.text } let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in diff --git a/Sources/ChatFeature/Controllers/MembersController.swift b/Sources/ChatFeature/Controllers/MembersController.swift index c2a7ea2a0bef4319213c6b28641b06ee4e9a4e99..e7bf2c3ece345ace155ed7babbf5c9ec05bf1caf 100644 --- a/Sources/ChatFeature/Controllers/MembersController.swift +++ b/Sources/ChatFeature/Controllers/MembersController.swift @@ -6,9 +6,9 @@ import XXModels final class MembersController: UIViewController { lazy private var stackView = UIStackView() - private let members: [GroupMember] + private let members: [Contact] - init(with members: [GroupMember]) { + init(with members: [Contact]) { self.members = members super.init(nibName: nil, bundle: nil) } @@ -26,16 +26,17 @@ final class MembersController: UIViewController { stackView.distribution = .fillEqually view.addSubview(stackView) - stackView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(10) - make.left.right.equalToSuperview() - make.bottom.equalTo(view.safeAreaLayoutGuide) + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.left.right.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide) } - for member in members { + members.forEach { let memberView = MemberView() -// memberView.titleLabel.text = member.username -// memberView.avatarView.setupProfile(title: member.username, image: member.photo, size: .small) + let assignedTitle = ($0.nickname ?? $0.username) ?? "Fetching username..." + memberView.titleLabel.text = assignedTitle + memberView.avatarView.setupProfile(title: assignedTitle, image: $0.photo, size: .small) stackView.addArrangedSubview(memberView) } } @@ -57,24 +58,24 @@ private final class MemberView: UIView { addSubview(avatarView) addSubview(separatorView) - avatarView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(10) - make.width.height.equalTo(30) - make.left.equalToSuperview().offset(25) - make.centerY.equalToSuperview() + avatarView.snp.makeConstraints { + $0.top.equalToSuperview().offset(10) + $0.width.height.equalTo(30) + $0.left.equalToSuperview().offset(25) + $0.centerY.equalToSuperview() } - titleLabel.snp.makeConstraints { make in - make.centerY.equalTo(avatarView) - make.left.equalTo(avatarView.snp.right).offset(14) - make.right.lessThanOrEqualToSuperview().offset(-10) + titleLabel.snp.makeConstraints { + $0.centerY.equalTo(avatarView) + $0.left.equalTo(avatarView.snp.right).offset(14) + $0.right.lessThanOrEqualToSuperview().offset(-10) } - separatorView.snp.makeConstraints { make in - make.height.equalTo(1) - make.left.equalToSuperview().offset(25) - make.right.equalToSuperview() - make.bottom.equalToSuperview() + separatorView.snp.makeConstraints { + $0.height.equalTo(1) + $0.left.equalToSuperview().offset(25) + $0.right.equalToSuperview() + $0.bottom.equalToSuperview() } } diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 9e4b4a5a8d858f72c0573881217425eb1e2f9e1b..7f611e182aa3dfdf95a7cc92567e93485afe9398 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -19,6 +19,10 @@ extension FlexibleSpace: CollectionCellContent { func prepareForReuse() {} } +extension Message: Differentiable { + public var differenceIdentifier: Int64 { id! } +} + public final class SingleChatController: UIViewController { @Dependency private var hud: HUDType @Dependency private var logger: XXLogger @@ -44,7 +48,7 @@ public final class SingleChatController: UIViewController { private let layoutDelegate = LayoutDelegate() private var cancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>() - private var sections = [ArraySection<ChatSection, ChatItem>]() + private var sections = [ArraySection<ChatSection, Message>]() private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor() var fileURL: URL? @@ -204,7 +208,9 @@ public final class SingleChatController: UIViewController { viewModel.replyPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] in inputComponent.setupReply(message: $0.text, sender: $0.sender) } + .sink { [unowned self] senderTitle, messageText in + inputComponent.setupReply(message: messageText, sender: senderTitle) + } .store(in: &cancellables) viewModel.navigation diff --git a/Sources/ChatFeature/Models/ChatItem.swift b/Sources/ChatFeature/Models/ChatItem.swift deleted file mode 100644 index 8694d9e54ec9727a18a24a2e9185a40ca262b6f1..0000000000000000000000000000000000000000 --- a/Sources/ChatFeature/Models/ChatItem.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Models -import XXModels -import Foundation -import DifferenceKit - -struct ChatItem: Equatable, Differentiable { - let date: Date - var uniqueId: Data? - let identity: Int64 - var roundURL: String? - let payload: Payload - var status: Message.Status - var differenceIdentifier: Int64 { identity } - - init(_ message: Message) { - self.identity = message.id! - self.status = message.status - self.payload = Payload(text: message.text, reply: nil) - self.roundURL = message.roundURL - self.uniqueId = message.networkId - self.date = message.date - } -} diff --git a/Sources/ChatFeature/Models/ChatSection.swift b/Sources/ChatFeature/Models/ChatSection.swift index cb755bf94795c6a69eb9549b5cacbb023aca1e65..a91c016fd166a1f7b2e41286186dd2cae1f2b340 100644 --- a/Sources/ChatFeature/Models/ChatSection.swift +++ b/Sources/ChatFeature/Models/ChatSection.swift @@ -2,11 +2,6 @@ import Foundation import DifferenceKit struct ChatSection: Equatable, Differentiable { - // MARK: Properties - var date: Date - - // MARK: DifferenceKit - var differenceIdentifier: Date { date } } diff --git a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift index 663d08f777ba945c7ec563975e6aa832c90008c4..7d611491a6824f1c0301a542a7d9e71e2810b7a8 100644 --- a/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/GroupChatViewModel.swift @@ -15,7 +15,7 @@ enum GroupChatNavigationRoutes: Equatable { final class GroupChatViewModel { @Dependency private var session: SessionType - var replyPublisher: AnyPublisher<ReplyModel, Never> { + var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() } @@ -23,18 +23,17 @@ final class GroupChatViewModel { routesSubject.eraseToAnyPublisher() } - let info: GroupChatInfo + let info: GroupInfo private var stagedReply: Reply? private var cancellables = Set<AnyCancellable>() - private let replySubject = PassthroughSubject<ReplyModel, Never>() + private let replySubject = PassthroughSubject<(String, String), Never>() private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>() - var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> { + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { session.dbManager.fetchMessagesPublisher(.init(chat: .group(info.group.id))) .assertNoFailure() - .map { messages -> [ArraySection<ChatSection, ChatItem>] in - let domainModels = messages.map { ChatItem($0) } - let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in + .map { messages -> [ArraySection<ChatSection, Message>] in + let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) return Calendar.current.date(from: components)! } @@ -43,14 +42,14 @@ final class GroupChatViewModel { .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } .sorted(by: { $0.model.date < $1.model.date }) } - .map { sections -> [ArraySection<ChatSection, ChatItem>] in - var snapshot = [ArraySection<ChatSection, ChatItem>]() + .map { sections -> [ArraySection<ChatSection, Message>] in + var snapshot = [ArraySection<ChatSection, Message>]() sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } return snapshot }.eraseToAnyPublisher() } - init(_ info: GroupChatInfo) { + init(_ info: GroupInfo) { self.info = info } @@ -60,8 +59,8 @@ final class GroupChatViewModel { _ = try? session.dbManager.bulkUpdateMessages(query, assignment) } - func didRequestDelete(_ items: [ChatItem]) { -// try? session.dbManager.deleteMessages(.init(id: items.map(\.identity))) + func didRequestDelete(_ messages: [Message]) { + _ = try? session.dbManager.deleteMessages(.init(id: Set(messages.map(\.id)))) } func send(_ text: String) { @@ -72,8 +71,9 @@ final class GroupChatViewModel { stagedReply = nil } - func retry(_ model: ChatItem) { -// session.retryGroupMessage(model.identity) + func retry(_ message: Message) { + guard let id = message.id else { return } + session.retryMessage(id) } func showRoundFrom(_ roundURL: String?) { @@ -89,20 +89,24 @@ final class GroupChatViewModel { } func getName(from senderId: Data) -> String { - fatalError() -// guard let member = info.members.first(where: { $0.userId == senderId }) else { return "You" } -// return member.username + guard let contact = try? session.dbManager.fetchContacts(.init(id: [senderId])).first else { + return "You" + } + + return (contact.nickname ?? contact.username) ?? "Fetching username..." } func getText(from messageId: Data) -> String { - fatalError() -// session.getTextFromGroupMessage(messageId: messageId) ?? "[DELETED]" + guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { + return "[DELETED]" + } + + return message.text } -// func didRequestReply(_ model: GroupChatItem) { -// guard let messageId = model.uniqueId else { return } -// -// stagedReply = Reply(messageId: messageId, senderId: model.sender) -// replySubject.send(.init(text: model.payload.text, sender: getName(from: model.sender))) -// } + func didRequestReply(_ message: Message) { + guard let networkId = message.networkId else { return } + stagedReply = Reply(messageId: networkId, senderId: message.senderId) + replySubject.send((getName(from: message.senderId), message.text)) + } } diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 86e7e3f9ec5083f27fbecd8aa6996da99977c5d1..8690ca1ccc9c850631e6f15fd798a41ab8c54c08 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -11,11 +11,6 @@ import Permissions import DifferenceKit import DependencyInjection -struct ReplyModel { - var text: String - var sender: String -} - enum SingleChatNavigationRoutes: Equatable { case none case camera @@ -36,22 +31,22 @@ final class SingleChatViewModel { private var stagedReply: Reply? private var cancellables = Set<AnyCancellable>() private let contactSubject: CurrentValueSubject<Contact, Never> - private let replySubject = PassthroughSubject<ReplyModel, Never>() + private let replySubject = PassthroughSubject<(String, String), Never>() private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() - private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, ChatItem>], Never>([]) + private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) var isOnline: AnyPublisher<Bool, Never> { session.isOnline } var contactPublisher: AnyPublisher<Contact, Never> { contactSubject.eraseToAnyPublisher() } - var replyPublisher: AnyPublisher<ReplyModel, Never> { replySubject.eraseToAnyPublisher() } + var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() } var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } - var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> { - sectionsRelay.map { sections -> [ArraySection<ChatSection, ChatItem>] in - var snapshot = [ArraySection<ChatSection, ChatItem>]() + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { + sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in + var snapshot = [ArraySection<ChatSection, Message>]() sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } return snapshot }.eraseToAnyPublisher() @@ -82,10 +77,8 @@ final class SingleChatViewModel { session.dbManager.fetchMessagesPublisher(.init(chat: .direct(session.myId, contact.id))) .assertNoFailure() - .map { messages in - - let domainModels = messages.map { ChatItem($0) } - let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in + .map { + let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) return Calendar.current.date(from: components)! } @@ -133,8 +126,9 @@ final class SingleChatViewModel { _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id))) } - func didRequestRetry(_ model: ChatItem) { - session.retryMessage(model.identity) + func didRequestRetry(_ message: Message) { + guard let id = message.id else { return } + session.retryMessage(id) } func didNavigateSomewhere() { @@ -167,11 +161,11 @@ final class SingleChatViewModel { return false } - func didRequestCopy(_ model: ChatItem) { - UIPasteboard.general.string = model.payload.text + func didRequestCopy(_ model: Message) { + UIPasteboard.general.string = model.text } - func didRequestDeleteSingle(_ model: ChatItem) { + func didRequestDeleteSingle(_ model: Message) { didRequestDelete([model]) } @@ -186,17 +180,25 @@ final class SingleChatViewModel { stagedReply = nil } - func didRequestReply(_ model: ChatItem) { -// guard let messageId = model.uniqueId else { return } -// -// let isIncoming = model.status == .received -// stagedReply = Reply(messageId: messageId, senderId: isIncoming ? contact.userId : session.myId) -// replySubject.send(.init(text: model.payload.text, sender: isIncoming ? contact.nickname ?? contact.username : "You")) + func didRequestReply(_ message: Message) { + let senderTitle: String = { + if message.senderId == session.myId { + return "You" + } else { + return (contact.nickname ?? contact.username) ?? "Fetching username..." + } + }() + + replySubject.send((senderTitle, message.text)) + stagedReply = Reply(messageId: message.networkId!, senderId: message.senderId) } func getText(from messageId: Data) -> String { - fatalError() -// session.getTextFromMessage(messageId: messageId) ?? "[DELETED]" + guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else { + return "[DELETED]" + } + + return message.text } func showRoundFrom(_ roundURL: String?) { @@ -207,19 +209,19 @@ final class SingleChatViewModel { } } - func didRequestDelete(_ items: [ChatItem]) { - ///session.delete(messages: items.map { $0.identity }) + func didRequestDelete(_ items: [Message]) { + _ = try? session.dbManager.deleteMessages(.init(id: Set(items.compactMap(\.id)))) } - func itemWith(id: Int64) -> ChatItem? { - sectionsRelay.value.flatMap(\.elements).first(where: { $0.identity == id }) + func itemWith(id: Int64) -> Message? { + sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id }) } func getName(from senderId: Data) -> String { senderId == session.myId ? "You" : contact.nickname ?? contact.username! } - func itemAt(indexPath: IndexPath) -> ChatItem? { + func itemAt(indexPath: IndexPath) -> Message? { guard sectionsRelay.value.count > indexPath.section else { return nil } let items = sectionsRelay.value[indexPath.section].elements diff --git a/Sources/ChatFeature/Views/GroupHeaderView.swift b/Sources/ChatFeature/Views/GroupHeaderView.swift index d85a49abe97265a8b0ba5bd1dec227b45e97c9ec..b33415707304f070f172764a5ee8a49d89a1a10d 100644 --- a/Sources/ChatFeature/Views/GroupHeaderView.swift +++ b/Sources/ChatFeature/Views/GroupHeaderView.swift @@ -1,6 +1,11 @@ import UIKit import Shared +struct Member { + let title: String + let photo: Data? +} + final class GroupHeaderView: UIView { let titleLabel = UILabel() let containerView = UIView() @@ -39,14 +44,14 @@ final class GroupHeaderView: UIView { required init?(coder: NSCoder) { nil } - func setup(title: String, memberList: [(String, Data?)]) { + func setup(title: String, memberList: [Member]) { titleLabel.text = title - for member in memberList { + memberList.forEach { let avatarView = AvatarView() avatarView.layer.borderWidth = 3 avatarView.layer.borderColor = UIColor.white.cgColor - avatarView.setupProfile(title: member.0, image: member.1, size: .small) + avatarView.setupProfile(title: $0.title, image: $0.photo, size: .small) avatarView.snp.makeConstraints { $0.width.height.equalTo(25.0) } stackView.addArrangedSubview(avatarView) } diff --git a/Sources/ChatListFeature/Controller/ChatListController.swift b/Sources/ChatListFeature/Controller/ChatListController.swift index d2d15a400f402ec532ee97a2407ccf88415c1c93..cd12b4ead14a92e07c8eab48337ec6df2845732a 100644 --- a/Sources/ChatListFeature/Controller/ChatListController.swift +++ b/Sources/ChatListFeature/Controller/ChatListController.swift @@ -3,9 +3,16 @@ import Theme import Models import Shared import Combine +import XXModels import MenuFeature import DependencyInjection +extension Contact: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + public final class ChatListController: UIViewController { @Dependency private var coordinator: ChatListCoordinating @Dependency private var statusBarController: StatusBarStyleControlling @@ -69,10 +76,10 @@ public final class ChatListController: UIViewController { } }.store(in: &cancellables) - viewModel.badgeCountPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in topLeftView.updateBadge($0) } - .store(in: &cancellables) +// viewModel.badgeCountPublisher +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in topLeftView.updateBadge($0) } +// .store(in: &cancellables) topLeftView.actionPublisher .receive(on: DispatchQueue.main) @@ -115,19 +122,19 @@ public final class ChatListController: UIViewController { collectionView: screenView.listContainerView.collectionView ) { collectionView, indexPath, contact in let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - cell.setup(title: contact.nickname ?? contact.username, image: contact.photo) + cell.setup(title: contact.nickname ?? contact.username!, image: contact.photo) return cell } screenView.listContainerView.collectionView.delegate = self screenView.listContainerView.collectionView.dataSource = collectionDataSource - viewModel.recentsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - collectionDataSource.apply($0) - shouldBeShowingRecents = $0.numberOfItems > 0 - }.store(in: &cancellables) +// viewModel.recentsPublisher +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in +// collectionDataSource.apply($0) +// shouldBeShowingRecents = $0.numberOfItems > 0 +// }.store(in: &cancellables) } private func setupBindings() { @@ -146,33 +153,33 @@ public final class ChatListController: UIViewController { screenView.searchListContainerView.emptyView.updateSearched(content: query) }.store(in: &cancellables) - Publishers.CombineLatest( - viewModel.searchPublisher, - screenView.searchView.textPublisher.removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [unowned self] items, query in - guard query.isEmpty == false else { - screenView.searchListContainerView.isHidden = true - screenView.listContainerView.isHidden = false - screenView.bringSubviewToFront(screenView.listContainerView) - return - } - - screenView.listContainerView.isHidden = true - screenView.searchListContainerView.isHidden = false - - guard items.numberOfItems > 0 else { - screenView.searchListContainerView.emptyView.isHidden = false - screenView.bringSubviewToFront(screenView.searchListContainerView) - screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) - return - } - - screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) - screenView.searchListContainerView.emptyView.isHidden = true - } - .store(in: &cancellables) +// Publishers.CombineLatest( +// viewModel.searchPublisher, +// screenView.searchView.textPublisher.removeDuplicates() +// ) +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] items, query in +// guard query.isEmpty == false else { +// screenView.searchListContainerView.isHidden = true +// screenView.listContainerView.isHidden = false +// screenView.bringSubviewToFront(screenView.listContainerView) +// return +// } +// +// screenView.listContainerView.isHidden = true +// screenView.searchListContainerView.isHidden = false +// +// guard items.numberOfItems > 0 else { +// screenView.searchListContainerView.emptyView.isHidden = false +// screenView.bringSubviewToFront(screenView.searchListContainerView) +// screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) +// return +// } +// +// screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) +// screenView.searchListContainerView.emptyView.isHidden = true +// } +// .store(in: &cancellables) screenView.searchView .isEditingPublisher @@ -181,19 +188,19 @@ public final class ChatListController: UIViewController { .sink { [unowned self] in isEditingSearch = $0 } .store(in: &cancellables) - viewModel.chatsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard $0.isEmpty == false else { - screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) - screenView.listContainerView.emptyView.isHidden = false - return - } - - screenView.listContainerView.bringSubviewToFront(tableController.view) - screenView.listContainerView.emptyView.isHidden = true - } - .store(in: &cancellables) +// viewModel.chatsPublisher +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in +// guard $0.isEmpty == false else { +// screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) +// screenView.listContainerView.emptyView.isHidden = false +// return +// } +// +// screenView.listContainerView.bringSubviewToFront(tableController.view) +// screenView.listContainerView.emptyView.isHidden = true +// } +// .store(in: &cancellables) screenView.searchListContainerView .emptyView.searchButton diff --git a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift index c1abc94ac78f3f3e95f1058a2e8ede9021d11616..dbef4946de693b49fe7361f943b291c21a7498d9 100644 --- a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift @@ -4,16 +4,16 @@ import Models import Combine import DependencyInjection -class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch snapshot().sectionIdentifiers[section] { - case .chats: - return "CHATS" - case .connections: - return "CONNECTIONS" - } - } -} +//class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { +// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { +// switch snapshot().sectionIdentifiers[section] { +// case .chats: +// return "CHATS" +// case .connections: +// return "CONNECTIONS" +// } +// } +//} final class ChatSearchTableController: UITableViewController { @Dependency private var coordinator: ChatListCoordinating @@ -21,65 +21,65 @@ final class ChatSearchTableController: UITableViewController { private let viewModel: ChatListViewModel private let cellHeight: CGFloat = 83.0 private var cancellables = Set<AnyCancellable>() - private var tableDataSource: ChatSearchListTableViewDiffableDataSource? +// private var tableDataSource: ChatSearchListTableViewDiffableDataSource? init(_ viewModel: ChatListViewModel) { self.viewModel = viewModel super.init(style: .grouped) - tableDataSource = ChatSearchListTableViewDiffableDataSource( - tableView: tableView - ) { table, indexPath, item in - let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) - switch item { - case .chat(let subitem): - if case .contact(let info) = subitem { - cell.setupContact( - name: info.contact.nickname ?? info.contact.username, - image: info.contact.photo, - date: Date.fromTimestamp(info.lastMessage!.timestamp), - hasUnread: info.lastMessage!.unread, - preview: info.lastMessage!.payload.text - ) - } - - if case .group(let info) = subitem { - let date: Date = { - guard let lastMessage = info.lastMessage else { - return info.group.createdAt - } - - return Date.fromTimestamp(lastMessage.timestamp) - }() - - let hasUnread: Bool = { - guard let lastMessage = info.lastMessage else { - return false - } - - return lastMessage.unread - }() - - cell.setupGroup( - name: info.group.name, - date: date, - preview: info.lastMessage?.payload.text, - hasUnread: hasUnread - ) - } - - case .connection(let contact): - cell.setupContact( - name: contact.nickname ?? contact.username, - image: contact.photo, - date: nil, - hasUnread: false, - preview: contact.username - ) - } - - return cell - } +// tableDataSource = ChatSearchListTableViewDiffableDataSource( +// tableView: tableView +// ) { table, indexPath, item in +// let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) +// switch item { +// case .chat(let subitem): +// if case .contact(let info) = subitem { +// cell.setupContact( +// name: info.contact.nickname ?? info.contact.username, +// image: info.contact.photo, +// date: Date.fromTimestamp(info.lastMessage!.timestamp), +// hasUnread: info.lastMessage!.unread, +// preview: info.lastMessage!.payload.text +// ) +// } +// +// if case .group(let info) = subitem { +// let date: Date = { +// guard let lastMessage = info.lastMessage else { +// return info.group.createdAt +// } +// +// return Date.fromTimestamp(lastMessage.timestamp) +// }() +// +// let hasUnread: Bool = { +// guard let lastMessage = info.lastMessage else { +// return false +// } +// +// return lastMessage.unread +// }() +// +// cell.setupGroup( +// name: info.group.name, +// date: date, +// preview: info.lastMessage?.payload.text, +// hasUnread: hasUnread +// ) +// } +// +// case .connection(let contact): +// cell.setupContact( +// name: contact.nickname ?? contact.username!, +// image: contact.photo, +// date: nil, +// hasUnread: false, +// preview: contact.username! +// ) +// } +// +// return cell +// } } required init?(coder: NSCoder) { nil } @@ -91,13 +91,13 @@ final class ChatSearchTableController: UITableViewController { tableView.tableFooterView = UIView() tableView.sectionIndexColor = .blue tableView.register(ChatListCell.self) - tableView.dataSource = tableDataSource +// tableView.dataSource = tableDataSource view.backgroundColor = Asset.neutralWhite.color - viewModel.searchPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } - .store(in: &cancellables) +// viewModel.searchPublisher +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } +// .store(in: &cancellables) } } @@ -110,19 +110,19 @@ extension ChatSearchTableController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let item = tableDataSource?.itemIdentifier(for: indexPath) { - switch item { - case .chat(let chat): - switch chat { - case .contact(let info): - guard info.contact.status == .friend else { return } - coordinator.toSingleChat(with: info.contact, from: self) - case .group(let info): - coordinator.toGroupChat(with: info, from: self) - } - case .connection(let contact): - coordinator.toContact(contact, from: self) - } - } +// if let item = tableDataSource?.itemIdentifier(for: indexPath) { +// switch item { +// case .chat(let chat): +// switch chat { +// case .contact(let info): +// guard info.contact.status == .friend else { return } +// coordinator.toSingleChat(with: info.contact, from: self) +// case .group(let info): +// coordinator.toGroupChat(with: info, from: self) +// } +// case .connection(let contact): +// coordinator.toContact(contact, from: self) +// } +// } } } diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift index 17b744113f0ecc43eb229f62cd5f4ddd5d8da385..ab8f5ec4e205f1161b73a5b678648d75feeee3b0 100644 --- a/Sources/ChatListFeature/Controller/ChatListTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift @@ -9,7 +9,7 @@ import DependencyInjection final class ChatListTableController: UITableViewController { @Dependency private var coordinator: ChatListCoordinating - private var rows = [Chat]() + private var rows = [Any]() private let viewModel: ChatListViewModel private let cellHeight: CGFloat = 83.0 private var cancellables = Set<AnyCancellable>() @@ -31,28 +31,28 @@ final class ChatListTableController: UITableViewController { tableView.register(ChatListCell.self) tableView.tableFooterView = UIView() - viewModel - .chatsPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] in - guard !self.rows.isEmpty else { - self.rows = $0 - tableView.reloadData() - return - } - - self.tableView.reload( - using: StagedChangeset(source: self.rows, target: $0), - deleteSectionsAnimation: .automatic, - insertSectionsAnimation: .automatic, - reloadSectionsAnimation: .none, - deleteRowsAnimation: .automatic, - insertRowsAnimation: .automatic, - reloadRowsAnimation: .none - ) { [unowned self] in - self.rows = $0 - } - }.store(in: &cancellables) +// viewModel +// .chatsPublisher +// .receive(on: DispatchQueue.main) +// .sink { [unowned self] in +// guard !self.rows.isEmpty else { +// self.rows = $0 +// tableView.reloadData() +// return +// } +// +// self.tableView.reload( +// using: StagedChangeset(source: self.rows, target: $0), +// deleteSectionsAnimation: .automatic, +// insertSectionsAnimation: .automatic, +// reloadSectionsAnimation: .none, +// deleteRowsAnimation: .automatic, +// insertRowsAnimation: .automatic, +// reloadRowsAnimation: .none +// ) { [unowned self] in +// self.rows = $0 +// } +// }.store(in: &cancellables) } } @@ -78,7 +78,7 @@ extension ChatListTableController { let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in guard let self = self else { return } - self.didRequestDeletionOf(self.rows[indexPath.row]) +// self.didRequestDeletionOf(self.rows[indexPath.row]) complete(true) } @@ -88,13 +88,13 @@ extension ChatListTableController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch rows[indexPath.row] { - case .contact(let info): - guard info.contact.status == .friend else { return } - coordinator.toSingleChat(with: info.contact, from: self) - case .group(let info): - coordinator.toGroupChat(with: info, from: self) - } +// switch rows[indexPath.row] { +// case .contact(let info): +// guard info.contact.status == .friend else { return } +// coordinator.toSingleChat(with: info.contact, from: self) +// case .group(let info): +// coordinator.toGroupChat(with: info, from: self) +// } } override func tableView( @@ -102,96 +102,97 @@ extension ChatListTableController { cellForRowAt indexPath: IndexPath ) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) - - if case .contact(let info) = rows[indexPath.row] { - cell.setupContact( - name: info.contact.nickname ?? info.contact.username, - image: info.contact.photo, - date: Date.fromTimestamp(info.lastMessage!.timestamp), - hasUnread: info.lastMessage!.unread, - preview: info.lastMessage!.payload.text - ) - } - - if case .group(let info) = rows[indexPath.row] { - let date: Date = { - guard let lastMessage = info.lastMessage else { - return info.group.createdAt - } - - return Date.fromTimestamp(lastMessage.timestamp) - }() - - let hasUnread: Bool = { - guard let lastMessage = info.lastMessage else { - return false - } - - return lastMessage.unread - }() - - cell.setupGroup( - name: info.group.name, - date: date, - preview: info.lastMessage?.payload.text, - hasUnread: hasUnread - ) - } - - return cell +// let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) +// +// if case .contact(let info) = rows[indexPath.row] { +// cell.setupContact( +// name: info.contact.nickname ?? info.contact.username, +// image: info.contact.photo, +// date: Date.fromTimestamp(info.lastMessage!.timestamp), +// hasUnread: info.lastMessage!.unread, +// preview: info.lastMessage!.payload.text +// ) +// } +// +// if case .group(let info) = rows[indexPath.row] { +// let date: Date = { +// guard let lastMessage = info.lastMessage else { +// return info.group.createdAt +// } +// +// return Date.fromTimestamp(lastMessage.timestamp) +// }() +// +// let hasUnread: Bool = { +// guard let lastMessage = info.lastMessage else { +// return false +// } +// +// return lastMessage.unread +// }() +// +// cell.setupGroup( +// name: info.group.name, +// date: date, +// preview: info.lastMessage?.payload.text, +// hasUnread: hasUnread +// ) +// } +// +// return cell + fatalError() } - private func didRequestDeletionOf(_ item: Chat) { - let title: String - let subtitle: String - let actionTitle: String - let actionClosure: () -> Void - - switch item { - case .group(let info): - title = Localized.ChatList.DeleteGroup.title - subtitle = Localized.ChatList.DeleteGroup.subtitle - actionTitle = Localized.ChatList.DeleteGroup.action - actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } - - case .contact(let info): - title = Localized.ChatList.Delete.title - subtitle = Localized.ChatList.Delete.subtitle - actionTitle = Localized.ChatList.Delete.delete - actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } - } - - let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerText( - font: Fonts.Mulish.regular.font(size: 16.0), - text: subtitle, - color: Asset.neutralBody.color, - alignment: .left, - lineHeightMultiple: 1.1, - spacingAfter: 39 - ), - actionButton - ]) - - actionButton.action.receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - actionClosure() - } - }.store(in: &drawerCancellables) - - coordinator.toDrawer(drawer, from: self) - } +// private func didRequestDeletionOf(_ item: Chat) { +// let title: String +// let subtitle: String +// let actionTitle: String +// let actionClosure: () -> Void +// +// switch item { +// case .group(let info): +// title = Localized.ChatList.DeleteGroup.title +// subtitle = Localized.ChatList.DeleteGroup.subtitle +// actionTitle = Localized.ChatList.DeleteGroup.action +// actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } +// +// case .contact(let info): +// title = Localized.ChatList.Delete.title +// subtitle = Localized.ChatList.Delete.subtitle +// actionTitle = Localized.ChatList.Delete.delete +// actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } +// } +// +// let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) +// +// let drawer = DrawerController(with: [ +// DrawerText( +// font: Fonts.Mulish.bold.font(size: 26.0), +// text: title, +// color: Asset.neutralActive.color, +// alignment: .left, +// spacingAfter: 19 +// ), +// DrawerText( +// font: Fonts.Mulish.regular.font(size: 16.0), +// text: subtitle, +// color: Asset.neutralBody.color, +// alignment: .left, +// lineHeightMultiple: 1.1, +// spacingAfter: 39 +// ), +// actionButton +// ]) +// +// actionButton.action.receive(on: DispatchQueue.main) +// .sink { +// drawer.dismiss(animated: true) { [weak self] in +// guard let self = self else { return } +// self.drawerCancellables.removeAll() +// actionClosure() +// } +// }.store(in: &drawerCancellables) +// +// coordinator.toDrawer(drawer, from: self) +// } } diff --git a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift index 42b7dda644aa04aa7f928ad8006d59715d442b4b..bb412859b16fe99591c8b658fc4b8798fe27fb00 100644 --- a/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift +++ b/Sources/ChatListFeature/Coordinator/ChatListCoordinator.swift @@ -1,6 +1,7 @@ import UIKit import Shared import Models +import XXModels import MenuFeature import ChatFeature import Presentation @@ -16,7 +17,7 @@ public protocol ChatListCoordinating { func toContact(_: Contact, from: UIViewController) func toSingleChat(with: Contact, from: UIViewController) func toDrawer(_: UIViewController, from: UIViewController) - func toGroupChat(with: GroupChatInfo, from: UIViewController) + func toGroupChat(with: GroupInfo, from: UIViewController) } public struct ChatListCoordinator: ChatListCoordinating { @@ -31,7 +32,7 @@ public struct ChatListCoordinator: ChatListCoordinating { var contactsFactory: () -> UIViewController var contactFactory: (Contact) -> UIViewController var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupChatInfo) -> UIViewController + var groupChatFactory: (GroupInfo) -> UIViewController var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController public init( @@ -41,7 +42,7 @@ public struct ChatListCoordinator: ChatListCoordinating { contactsFactory: @escaping () -> UIViewController, contactFactory: @escaping (Contact) -> UIViewController, singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupChatInfo) -> UIViewController, + groupChatFactory: @escaping (GroupInfo) -> UIViewController, sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController ) { self.scanFactory = scanFactory @@ -81,7 +82,7 @@ public extension ChatListCoordinator { pushPresenter.present(screen, from: parent) } - func toGroupChat(with group: GroupChatInfo, from parent: UIViewController) { + func toGroupChat(with group: GroupInfo, from parent: UIViewController) { let screen = groupChatFactory(group) pushPresenter.present(screen, from: parent) } diff --git a/Sources/ChatListFeature/Models/Chat.swift b/Sources/ChatListFeature/Models/Chat.swift index 3e159479aeaade0a5b1355d19eccf627f5d5f3b7..80106ee6bb532c08d444d00de9540afe95380ca6 100644 --- a/Sources/ChatListFeature/Models/Chat.swift +++ b/Sources/ChatListFeature/Models/Chat.swift @@ -1,34 +1,35 @@ -import Models -import Foundation -import DifferenceKit - -enum Chat: Equatable, Differentiable, Hashable { - case group(GroupChatInfo) - case contact(SingleChatInfo) - - var differenceIdentifier: Data { - switch self { - case .contact(let info): - return info.contact.userId - case .group(let info): - return info.group.groupId - } - } - - var orderingDate: Date { - switch self { - case .group(let info): - if let lastMessage = info.lastMessage { - return Date.fromTimestamp(lastMessage.timestamp) - } else { - return info.group.createdAt - } - case .contact(let info): - guard let lastMessage = info.lastMessage else { - fatalError("Should have an E2E chat without a last message") - } - - return Date.fromTimestamp(lastMessage.timestamp) - } - } -} +//import Models +//import XXModels +//import Foundation +//import DifferenceKit +// +//enum Chat: Equatable, Differentiable, Hashable { +// case group(GroupChatInfo) +// case contact(SingleChatInfo) +// +// var differenceIdentifier: Data { +// switch self { +// case .contact(let info): +// return info.contact.userId +// case .group(let info): +// return info.group.groupId +// } +// } +// +// var orderingDate: Date { +// switch self { +// case .group(let info): +// if let lastMessage = info.lastMessage { +// return Date.fromTimestamp(lastMessage.timestamp) +// } else { +// return info.group.createdAt +// } +// case .contact(let info): +// guard let lastMessage = info.lastMessage else { +// fatalError("Should have an E2E chat without a last message") +// } +// +// return Date.fromTimestamp(lastMessage.timestamp) +// } +// } +//} diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index ceb096f383a164fd6ead74e9819089b4e45a5807..a7cffa120aa560aa5827f6e964048427705faadd 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -3,6 +3,7 @@ import UIKit import Shared import Models import Combine +import XXModels import Defaults import Integration import DependencyInjection @@ -12,13 +13,13 @@ enum SearchSection { case connections } -enum SearchItem: Equatable, Hashable { - case chat(Chat) - case connection(Contact) -} +//enum SearchItem: Equatable, Hashable { +// case chat(Chat) +// case connection(Contact) +//} typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact> -typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> +//typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> final class ChatListViewModel { @Dependency private var session: SessionType @@ -27,96 +28,96 @@ final class ChatListViewModel { session.isOnline } - var chatsPublisher: AnyPublisher<[Chat], Never> { - chatsSubject.eraseToAnyPublisher() - } +// var chatsPublisher: AnyPublisher<[Chat], Never> { +// chatsSubject.eraseToAnyPublisher() +// } var hudPublisher: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() } - var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { - session.contacts(.isRecent).map { - let section = SectionId() - var snapshot = RecentsSnapshot() - snapshot.appendSections([section]) - snapshot.appendItems($0, toSection: section) - return snapshot - }.eraseToAnyPublisher() - } - - var searchPublisher: AnyPublisher<SearchSnapshot, Never> { - Publishers.CombineLatest3( - session.contacts(.all), - chatsPublisher, - searchSubject - .removeDuplicates() - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) - .eraseToAnyPublisher() - ) - .map { (contacts, chats, query) in - let connectionItems = contacts.filter { - let username = $0.username.lowercased().contains(query.lowercased()) - let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false - return username || nickname - }.map(SearchItem.connection) - - let chatItems = chats.filter { - switch $0 { - case .contact(let info): - let username = info.contact.username.lowercased().contains(query.lowercased()) - let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false - let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false - return username || nickname || lastMessage - - case .group(let info): - let name = info.group.name.lowercased().contains(query.lowercased()) - let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false - return name || last - } - }.map(SearchItem.chat) - - var snapshot = SearchSnapshot() - - if connectionItems.count > 0 { - snapshot.appendSections([.connections]) - snapshot.appendItems(connectionItems, toSection: .connections) - } - - if chatItems.count > 0 { - snapshot.appendSections([.chats]) - snapshot.appendItems(chatItems, toSection: .chats) - } - - return snapshot - }.eraseToAnyPublisher() - } - - var badgeCountPublisher: AnyPublisher<Int, Never> { - Publishers.CombineLatest( - session.contacts(.received), - session.groups(.pending) - ) - .map { $0.0.count + $0.1.count } - .eraseToAnyPublisher() - } +// var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { +// session.contacts(.isRecent).map { +// let section = SectionId() +// var snapshot = RecentsSnapshot() +// snapshot.appendSections([section]) +// snapshot.appendItems($0, toSection: section) +// return snapshot +// }.eraseToAnyPublisher() +// } + +// var searchPublisher: AnyPublisher<SearchSnapshot, Never> { +// Publishers.CombineLatest3( +// session.contacts(.all), +// chatsPublisher, +// searchSubject +// .removeDuplicates() +// .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) +// .eraseToAnyPublisher() +// ) +// .map { (contacts, chats, query) in +// let connectionItems = contacts.filter { +// let username = $0.username.lowercased().contains(query.lowercased()) +// let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false +// return username || nickname +// }.map(SearchItem.connection) +// +// let chatItems = chats.filter { +// switch $0 { +// case .contact(let info): +// let username = info.contact.username.lowercased().contains(query.lowercased()) +// let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false +// let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false +// return username || nickname || lastMessage +// +// case .group(let info): +// let name = info.group.name.lowercased().contains(query.lowercased()) +// let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false +// return name || last +// } +// }.map(SearchItem.chat) +// +// var snapshot = SearchSnapshot() +// +// if connectionItems.count > 0 { +// snapshot.appendSections([.connections]) +// snapshot.appendItems(connectionItems, toSection: .connections) +// } +// +// if chatItems.count > 0 { +// snapshot.appendSections([.chats]) +// snapshot.appendItems(chatItems, toSection: .chats) +// } +// +// return snapshot +// }.eraseToAnyPublisher() +// } +// +// var badgeCountPublisher: AnyPublisher<Int, Never> { +// Publishers.CombineLatest( +// session.contacts(.received), +// session.groups(.pending) +// ) +// .map { $0.0.count + $0.1.count } +// .eraseToAnyPublisher() +// } private var cancellables = Set<AnyCancellable>() private let searchSubject = CurrentValueSubject<String, Never>("") - private let chatsSubject = CurrentValueSubject<[Chat], Never>([]) +// private let chatsSubject = CurrentValueSubject<[Chat], Never>([]) private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) init() { - Publishers.CombineLatest( - session.singleChats(.all), - session.groupChats(.accepted) - ).map { - let groups = $0.1.map(Chat.group) - let chats = $0.0.map(Chat.contact) - return (chats + groups).sorted { $0.orderingDate > $1.orderingDate } - } - .sink { [unowned self] in chatsSubject.send($0) } - .store(in: &cancellables) +// Publishers.CombineLatest( +// session.singleChats(.all), +// session.groupChats(.accepted) +// ).map { +// let groups = $0.1.map(Chat.group) +// let chats = $0.0.map(Chat.contact) +// return (chats + groups).sorted { $0.orderingDate > $1.orderingDate } +// } +// .sink { [unowned self] in chatsSubject.send($0) } +// .store(in: &cancellables) } func updateSearch(query: String) { @@ -128,7 +129,7 @@ final class ChatListViewModel { do { try session.leave(group: group) - session.deleteAll(from: group) + try session.dbManager.deleteMessages(.init(chat: .group(group.id))) hudSubject.send(.none) } catch { hudSubject.send(.error(.init(with: error))) @@ -136,6 +137,6 @@ final class ChatListViewModel { } func clear(_ contact: Contact) { - session.deleteAll(from: contact) + _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id))) } } diff --git a/Sources/ContactListFeature/Controllers/ContactListTableController.swift b/Sources/ContactListFeature/Controllers/ContactListTableController.swift index 8acfc91190671f875f840b82073935505c0f1d8b..f940366ae08cfe77f9cd54e5d1008a55b7149bc6 100644 --- a/Sources/ContactListFeature/Controllers/ContactListTableController.swift +++ b/Sources/ContactListFeature/Controllers/ContactListTableController.swift @@ -47,8 +47,9 @@ final class ContactListTableController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: SmallAvatarAndTitleCell = tableView.dequeueReusableCell(forIndexPath: indexPath) let contact = sections[indexPath.section][indexPath.row] - cell.titleLabel.text = contact.nickname ?? contact.username - cell.avatarView.setupProfile(title: contact.nickname ?? contact.username, image: contact.photo, size: .medium) + let name = (contact.nickname ?? contact.username) ?? "Fetching username..." + cell.titleLabel.text = name + cell.avatarView.setupProfile(title: name, image: contact.photo, size: .medium) return cell } diff --git a/Sources/ContactListFeature/Controllers/CreateGroupController.swift b/Sources/ContactListFeature/Controllers/CreateGroupController.swift index f5a0ebd656b8bd41e216342ccceddd98f3338c00..5d0690ffa0869de661b537adbd86f27df93527e9 100644 --- a/Sources/ContactListFeature/Controllers/CreateGroupController.swift +++ b/Sources/ContactListFeature/Controllers/CreateGroupController.swift @@ -74,7 +74,7 @@ public final class CreateGroupController: UIViewController { ) { [weak viewModel] collectionView, indexPath, contact in let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) - cell.setup(title: contact.nickname ?? contact.username, image: contact.photo) + cell.setup(title: contact.nickname ?? contact.username!, image: contact.photo) cell.didTapRemove = { viewModel?.didSelect(contact: contact) } return cell @@ -85,7 +85,7 @@ public final class CreateGroupController: UIViewController { ) { [weak self] tableView, indexPath, contact in let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self) cell.titleLabel.text = contact.nickname ?? contact.username - cell.avatarView.setupProfile(title: contact.nickname ?? contact.username, image: contact.photo, size: .medium) + cell.avatarView.setupProfile(title: contact.nickname ?? contact.username!, image: contact.photo, size: .medium) if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) @@ -183,3 +183,9 @@ extension CreateGroupController: UITableViewDelegate { } } } + +extension Contact: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift b/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift index 8cf906fa621df4ae1dca668b40df77cd2ae14149..baf38264a85467038888c2b37e618598a7190acd 100644 --- a/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift +++ b/Sources/ContactListFeature/Coordinator/ContactListCoordinator.swift @@ -16,7 +16,7 @@ public protocol ContactListCoordinating { func toSideMenu(from: UIViewController) func toContact(_: Contact, from: UIViewController) func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupChatInfo, from: UIViewController) + func toGroupChat(with: GroupInfo, from: UIViewController) func toGroupDrawer(with: Int, from: UIViewController, _: @escaping (String, String?) -> Void) } @@ -32,7 +32,7 @@ public struct ContactListCoordinator: ContactListCoordinating { var requestsFactory: () -> UIViewController var contactFactory: (Contact) -> UIViewController var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupChatInfo) -> UIViewController + var groupChatFactory: (GroupInfo) -> UIViewController var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController var groupDrawerFactory: (Int, @escaping (String, String?) -> Void) -> UIViewController @@ -43,7 +43,7 @@ public struct ContactListCoordinator: ContactListCoordinating { requestsFactory: @escaping () -> UIViewController, contactFactory: @escaping (Contact) -> UIViewController, singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupChatInfo) -> UIViewController, + groupChatFactory: @escaping (GroupInfo) -> UIViewController, sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, groupDrawerFactory: @escaping (Int, @escaping (String, String?) -> Void) -> UIViewController ) { @@ -102,7 +102,7 @@ public extension ContactListCoordinator { pushPresenter.present(screen, from: parent) } - func toGroupChat(with info: GroupChatInfo, from parent: UIViewController) { + func toGroupChat(with info: GroupInfo, from parent: UIViewController) { let screen = groupChatFactory(info) pushPresenter.present(screen, from: parent) } diff --git a/Sources/ContactListFeature/Helpers/IndexedListCollator.swift b/Sources/ContactListFeature/Helpers/IndexedListCollator.swift new file mode 100644 index 0000000000000000000000000000000000000000..249af6fad35653a205a8d485dbab088cc8ad9578 --- /dev/null +++ b/Sources/ContactListFeature/Helpers/IndexedListCollator.swift @@ -0,0 +1,53 @@ +import UIKit +import XXModels + +public protocol IndexableItem { + var indexedOn: NSString { get } +} + +class IndexedListCollator<Item: IndexableItem> { + private final class CollationWrapper: NSObject { + let value: Any + @objc let indexedOn: NSString + + init(value: Any, indexedOn: NSString) { + self.value = value + self.indexedOn = indexedOn + } + + func unwrappedValue<UnwrappedType>() -> UnwrappedType { + return value as! UnwrappedType + } + } + + public init() {} + + public func sectioned(items: [Item]) -> (sections: [[Item]], collation: UILocalizedIndexedCollation) { + let collation = UILocalizedIndexedCollation.current() + let selector = #selector(getter: CollationWrapper.indexedOn) + + let wrappedItems = items.map { item in + CollationWrapper(value: item, indexedOn: item.indexedOn) + } + + let sortedObjects = collation.sortedArray(from: wrappedItems, collationStringSelector: selector) as! [CollationWrapper] + + var sections = collation.sectionIndexTitles.map { _ in [Item]() } + sortedObjects.forEach { item in + let sectionNumber = collation.section(for: item, collationStringSelector: selector) + sections[sectionNumber].append(item.unwrappedValue()) + } + + return (sections: sections.filter { !$0.isEmpty }, collation: collation) + } +} + +extension Contact: IndexableItem { + public var indexedOn: NSString { + guard let nickname = nickname else { + return "\(username!.first!)" as NSString + } + + return "\(nickname.first!)" as NSString + } +} diff --git a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift index 72e4582e977fae4f0377923baf099fb754cbcc79..a21ec093a9c251354bd208804076212166a749d4 100644 --- a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift @@ -8,27 +8,26 @@ final class ContactListViewModel { @Dependency private var session: SessionType var contacts: AnyPublisher<[Contact], Never> { - session.contacts(.friends).eraseToAnyPublisher() + session.dbManager.fetchContactsPublisher(.init(authStatus: [.friend])) + .assertNoFailure() + .eraseToAnyPublisher() } var requestCount: AnyPublisher<Int, Never> { - Publishers.CombineLatest( - session.contacts(.received), - session.groups(.pending) - ).map { (contacts, groups) in - let contactRequests = contacts.filter { - $0.status == .verified || - $0.status == .confirming || - $0.status == .confirmationFailed || - $0.status == .verificationFailed || - $0.status == .verificationInProgress - } + let groupQuery = Group.Query(authStatus: [.pending]) + let contactsQuery = Contact.Query(authStatus: [ + .verified, + .confirming, + .confirmationFailed, + .verificationFailed, + .verificationInProgress + ]) - let groupRequests = groups.filter { - $0.status == .pending - } - - return contactRequests.count + groupRequests.count - }.eraseToAnyPublisher() + return Publishers.CombineLatest( + session.dbManager.fetchContactsPublisher(contactsQuery).catch { _ in Just([]) }, + session.dbManager.fetchGroupsPublisher(groupQuery).catch { _ in Just([]) } + ) + .map { $0.0.count + $0.1.count } + .eraseToAnyPublisher() } } diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index 3c421bfe59b5640d00f6f371a3b7a87dd4ba71e1..38717ac21153c846c1165fa023369fc7b15305f1 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -28,13 +28,13 @@ final class CreateGroupViewModel { hudRelay.eraseToAnyPublisher() } - var info: AnyPublisher<GroupChatInfo, Never> { + var info: AnyPublisher<GroupInfo, Never> { infoRelay.eraseToAnyPublisher() } private var allContacts = [Contact]() private var cancellables = Set<AnyCancellable>() - private let infoRelay = PassthroughSubject<GroupChatInfo, Never>() + private let infoRelay = PassthroughSubject<GroupInfo, Never>() private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) private let contactsRelay = CurrentValueSubject<[Contact], Never>([]) private let selectedContactsRelay = CurrentValueSubject<[Contact], Never>([]) @@ -42,8 +42,9 @@ final class CreateGroupViewModel { // MARK: Lifecycle init() { - session.contacts(.friends) - .map { $0.sorted(by: { $0.username < $1.username })} + session.dbManager.fetchContactsPublisher(.init(authStatus: [.friend])) + .assertNoFailure() + .map { $0.sorted(by: { $0.username! < $1.username! })} .sink { [unowned self] in allContacts = $0 contactsRelay.send($0) @@ -66,7 +67,7 @@ final class CreateGroupViewModel { return } - contactsRelay.send(allContacts.filter { $0.username.contains(text.lowercased()) }) + contactsRelay.send(allContacts.filter { $0.username!.contains(text.lowercased()) }) } func create(name: String, welcome: String?, members: [Contact]) { @@ -78,8 +79,8 @@ final class CreateGroupViewModel { self.hudRelay.send(.none) switch $0 { - case .success((let group, let members)): - self.infoRelay.send(.init(group: group, members: members)) + case .success(let info): + self.infoRelay.send(info) case .failure(let error): self.hudRelay.send(.error(.init(with: error))) } diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift index 61ccceb42f58ccae7adc40a76c4e13d230f53a47..8161105f3a53739334440786612165dd342b3b90 100644 --- a/Sources/Integration/Mocks/BindingsMock.swift +++ b/Sources/Integration/Mocks/BindingsMock.swift @@ -120,7 +120,7 @@ public final class BindingsMock: BindingsInterface { self?.requestsSubject.send(.carlRequested) self?.requestsSubject.send(.angelinaRequested) self?.requestsSubject.send(.elonRequested) - self?.groupRequestsSubject.send(.mockGroup) + //self?.groupRequestsSubject.send(.mockGroup) DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in self?.confirmationsSubject.send(.georgeDiscovered) diff --git a/Sources/Integration/Session/Session+Group.swift b/Sources/Integration/Session/Session+Group.swift index ced2ada351cc2d0bb1ae5bb120a257be306565e7..b0bd6efd513b2118468f459e0041db9eac7d55ab 100644 --- a/Sources/Integration/Session/Session+Group.swift +++ b/Sources/Integration/Session/Session+Group.swift @@ -2,8 +2,6 @@ import Models import XXModels import Foundation -public typealias GroupCompletion = (Result<(Group, [GroupMember]), Error>) -> Void - extension Session { public func join(group: Group) throws { guard let manager = client.groupManager else { fatalError("A group manager was not created") } @@ -21,7 +19,12 @@ extension Session { try dbManager.deleteGroup(group) } - public func createGroup(name: String, welcome: String?, members: [Contact], _ completion: @escaping GroupCompletion) { + public func createGroup( + name: String, + welcome: String?, + members: [Contact], + _ completion: @escaping (Result<GroupInfo, Error>) -> Void + ) { guard let manager = client.groupManager else { fatalError("A group manager was not created") } let me = client.bindings.meMarshalled @@ -32,8 +35,7 @@ extension Session { switch $0 { case .success(let group): - completion(.success((group, self.processGroupCreation(group, memberIds: memberIds, welcome: welcome)))) - break + completion(.success(self.processGroupCreation(group, memberIds: memberIds, welcome: welcome))) case .failure(let error): completion(.failure(error)) } @@ -41,60 +43,47 @@ extension Session { } @discardableResult - func processGroupCreation(_ group: Group, memberIds: [Data], welcome: String?) -> [GroupMember] { - // TODO: Implement this checking on which members of the group are my members etc. + func processGroupCreation(_ group: Group, memberIds: [Data], welcome: String?) -> GroupInfo { + /// Save the group + /// + _ = try? dbManager.saveGroup(group) + + /// Save the members + /// + memberIds.forEach { _ = try? dbManager.saveGroupMember(.init(groupId: group.id, contactId: $0)) } + + /// Save the welcome message (if any) + /// + if let welcome = welcome { + _ = try? dbManager.saveMessage(.init( + networkId: nil, + senderId: group.leaderId, + recipientId: client.bindings.meMarshalled, + groupId: group.id, + date: Date(), + status: .received, + isUnread: true, + text: welcome, + replyMessageId: nil, + roundURL: nil, + fileTransferId: nil + )) + } -// try! dbManager.saveGroup(group) -// -// var members: [GroupMember] = [] -// -// if let contactsOnGroup: [Contact] = try? dbManager.fetchContacts(.init(id: Set(memberIds)) { -// //contactsOnGroup.forEach { members.append(GroupMember(contact: $0, group: group)) } -// } -// -// let strangersOnGroup = memberIds -// .filter { !members.map { $0.contactId }.contains($0) } -// .filter { $0 != client.bindings.myId } -// -// if !strangersOnGroup.isEmpty { -// for stranger in strangersOnGroup.enumerated() { -// members.append(GroupMember( -// userId: stranger.element, -// groupId: group.groupId, -// status: .pendingUsername, -// username: "Fetching username...", -// photo: nil -// )) -// } -// } -// -// members.forEach { try! dbManager.saveGroupMember($0) } -// -// if group.leaderId != client.bindings.meMarshalled, inappnotifications { -// DeviceFeedback.sound(.contactAdded) -// DeviceFeedback.shake(.notification) -// } -// -// scanStrangers {} -// -// if let welcome = welcome { -// _ = try? dbManager.saveMessage(.init( -// networkId: nil, -// senderId: group.leaderId, -// recipientId: client.bindings.meMarshalled, -// groupId: group.id, -// date: Date(), -// status: .received, -// isUnread: true, -// text: welcome, -// replyMessageId: nil, -// roundURL: nil, -// fileTransferId: nil -// )) -// } -// -// return members - fatalError() + /// Buzz if the group was not created by me + /// + if group.leaderId != client.bindings.meMarshalled, inappnotifications { + DeviceFeedback.sound(.contactAdded) + DeviceFeedback.shake(.notification) + } + + scanStrangers {} + + guard let info = try? dbManager.fetchGroupInfos(.init(groupId: group.id)).first else { + fatalError() + } + + return info } } @@ -162,7 +151,7 @@ extension Session { } public func scanStrangers(_ completion: @escaping () -> Void) { - // TODO: How this will work? + // TODO: Needs a request for Contacts without username set // DispatchQueue.global().async { [weak self] in // guard let self = self, let ud = self.client.userDiscovery else { return } diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift index 19b020dbcca50e9e9bb43a64d46faf02cc569064..fbb531ed07795dd6d22fea7fc1409ddfb85da11a 100644 --- a/Sources/Integration/Session/SessionType.swift +++ b/Sources/Integration/Session/SessionType.swift @@ -63,6 +63,6 @@ public protocol SessionType { name: String, welcome: String?, members: [Contact], - _ completion: @escaping (Result<(Group, [GroupMember]), Error>) -> Void + _ completion: @escaping (Result<GroupInfo, Error>) -> Void ) } diff --git a/Sources/LaunchFeature/LaunchCoordinator.swift b/Sources/LaunchFeature/LaunchCoordinator.swift index 59f6c5f2f5ed421f2109c6226d154d8e85c1a190..4f5a56291b19634df4c46edc2634acb55ba5812e 100644 --- a/Sources/LaunchFeature/LaunchCoordinator.swift +++ b/Sources/LaunchFeature/LaunchCoordinator.swift @@ -8,7 +8,7 @@ public protocol LaunchCoordinating { func toRequests(from: UIViewController) func toOnboarding(with: String, from: UIViewController) func toSingleChat(with: Contact, from: UIViewController) - func toGroupChat(with: GroupChatInfo, from: UIViewController) + func toGroupChat(with: GroupInfo, from: UIViewController) } public struct LaunchCoordinator: LaunchCoordinating { @@ -18,14 +18,14 @@ public struct LaunchCoordinator: LaunchCoordinating { var chatListFactory: () -> UIViewController var onboardingFactory: (String) -> UIViewController var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupChatInfo) -> UIViewController + var groupChatFactory: (GroupInfo) -> UIViewController public init( requestsFactory: @escaping () -> UIViewController, chatListFactory: @escaping () -> UIViewController, onboardingFactory: @escaping (String) -> UIViewController, singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupChatInfo) -> UIViewController + groupChatFactory: @escaping (GroupInfo) -> UIViewController ) { self.requestsFactory = requestsFactory self.chatListFactory = chatListFactory @@ -57,7 +57,7 @@ public extension LaunchCoordinator { replacePresenter.present(chatListScreen, singleChatScreen, from: parent) } - func toGroupChat(with group: GroupChatInfo, from parent: UIViewController) { + func toGroupChat(with group: GroupInfo, from parent: UIViewController) { let chatListScreen = chatListFactory() let groupChatScreen = groupChatFactory(group) replacePresenter.present(chatListScreen, groupChatScreen, from: parent) diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 0eb26949bdbcb0d3b9382dc1f53b90726faf4ae9..054432b4403e7177bf129c21680a58ac4abb6b12 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -120,23 +120,21 @@ final class LaunchViewModel { } func getContactWith(userId: Data) -> Contact? { - fatalError() -// guard let session = try? DependencyInjection.Container.shared.resolve() as SessionType, -// let contact = session.getContactWith(userId: userId) else { -// return nil -// } -// -// return contact + guard let session = try? DependencyInjection.Container.shared.resolve() as SessionType, + let contact = try? session.dbManager.fetchContacts(.init(id: [userId])).first else { + return nil + } + + return contact } - func getGroupInfoWith(groupId: Data) -> GroupChatInfo? { - fatalError() -// guard let session: SessionType = try? DependencyInjection.Container.shared.resolve(), -// let info = session.getGroupChatInfoWith(groupId: groupId) else { -// return nil -// } -// -// return info + func getGroupInfoWith(groupId: Data) -> GroupInfo? { + guard let session: SessionType = try? DependencyInjection.Container.shared.resolve(), + let info = try? session.dbManager.fetchGroupInfos(.init(groupId: groupId)).first else { + return nil + } + + return info } private func versionFailed(error: Error) { diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index c92d5cb26e82317750b6b039bad65c563c1f7eea..e9de6a320d86525050224105aa8e6fa1d374b733 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -3,6 +3,7 @@ import UIKit import Models import Shared import Combine +import XXModels import Countries import ToastFeature import DrawerFeature @@ -110,10 +111,10 @@ extension RequestsReceivedController: UICollectionViewDelegate { switch request { case .group(let group): - guard group.status == .pending || group.status == .hidden else { return } + guard group.authStatus == .pending || group.authStatus == .hidden else { return } presentGroupRequestDrawer(forGroup: group) case .contact(let contact): - guard contact.status == .verified || contact.status == .hidden else { return } + guard contact.authStatus == .verified || contact.authStatus == .hidden else { return } presentSingleRequestDrawer(forContact: contact) } } @@ -211,7 +212,7 @@ extension RequestsReceivedController { let drawerNickname = DrawerText( font: Fonts.Mulish.extraBold.font(size: 26.0), - text: contact.nickname ?? contact.username, + text: contact.nickname ?? contact.username!, color: Asset.neutralDark.color, spacingAfter: 20 ) @@ -392,7 +393,7 @@ extension RequestsReceivedController { let drawerUsername = DrawerText( font: Fonts.Mulish.extraBold.font(size: 26.0), - text: contact.username, + text: contact.username!, color: Asset.neutralDark.color, spacingAfter: 25 ) @@ -452,7 +453,7 @@ extension RequestsReceivedController { items.append(drawerNicknameTitle) let drawerNicknameInput = DrawerInput( - placeholder: contact.username, + placeholder: contact.username!, validator: .init( wrongIcon: .image(Asset.sharedError.image), correctIcon: .image(Asset.sharedSuccess.image), diff --git a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift b/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift index 9a570bc2172552dbed46a50181c0fac862e940cd..3c798cfc0243bffbbf54939c37d7ea7eb90c1294 100644 --- a/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift +++ b/Sources/RequestsFeature/Coordinator/RequestsCoordinator.swift @@ -12,8 +12,8 @@ public protocol RequestsCoordinating { func toSideMenu(from: UIViewController) func toContact(_: Contact, from: UIViewController) func toSingleChat(with: Contact, from: UIViewController) + func toGroupChat(with: GroupInfo, from: UIViewController) func toDrawer(_: UIViewController, from: UIViewController) - func toGroupChat(with: GroupChatInfo, from: UIViewController) func toDrawerBottom(_: UIViewController, from: UIViewController) func toNickname(from: UIViewController, prefilled: String, _: @escaping StringClosure) } @@ -27,7 +27,7 @@ public struct RequestsCoordinator: RequestsCoordinating { var searchFactory: () -> UIViewController var contactFactory: (Contact) -> UIViewController var singleChatFactory: (Contact) -> UIViewController - var groupChatFactory: (GroupChatInfo) -> UIViewController + var groupChatFactory: (GroupInfo) -> UIViewController var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController var nicknameFactory: (String, @escaping StringClosure) -> UIViewController @@ -35,7 +35,7 @@ public struct RequestsCoordinator: RequestsCoordinating { searchFactory: @escaping () -> UIViewController, contactFactory: @escaping (Contact) -> UIViewController, singleChatFactory: @escaping (Contact) -> UIViewController, - groupChatFactory: @escaping (GroupChatInfo) -> UIViewController, + groupChatFactory: @escaping (GroupInfo) -> UIViewController, sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController, nicknameFactory: @escaping (String, @escaping StringClosure) -> UIViewController ) { @@ -58,7 +58,7 @@ public extension RequestsCoordinator { } func toGroupChat( - with info: GroupChatInfo, + with info: GroupInfo, from parent: UIViewController ) { let screen = groupChatFactory(info) diff --git a/Sources/RequestsFeature/Models/Request.swift b/Sources/RequestsFeature/Models/Request.swift index 097b55b85aa4e2efe4a106c44e2efda30a5dc9d6..2048d39962a7ccb968a6aa38759892b48c47ba63 100644 --- a/Sources/RequestsFeature/Models/Request.swift +++ b/Sources/RequestsFeature/Models/Request.swift @@ -16,7 +16,7 @@ enum Request: Hashable, Equatable { case .group: return .verified case .contact(let contact): - return contact.status.toRequestStatus() + return contact.authStatus.toRequestStatus() } } @@ -41,7 +41,7 @@ enum RequestStatus { case failedToRequest } -extension Contact.Status { +extension Contact.AuthStatus { func toRequestStatus() -> RequestStatus { switch self { case .friend, .stranger: @@ -67,3 +67,15 @@ extension Contact.Status { } } } + +extension Contact: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Group: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift index f67882bb3fca1237ac3f138b549d262755e7fafe..9214c9ef3f19d337ae66f0f9a6c8368741ff1096 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsFailedViewModel.swift @@ -24,7 +24,8 @@ final class RequestsFailedViewModel { var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() init() { - session.contacts(.failed) + session.dbManager.fetchContactsPublisher(.init(authStatus: [.requestFailed])) + .assertNoFailure() .map { data -> NSDiffableDataSourceSnapshot<Section, Request> in var snapshot = NSDiffableDataSourceSnapshot<Section, Request>() snapshot.appendSections([.appearing]) diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index e231498dbb03f04bb7ae1267fb9884d556f1f8a4..52a4fe660deefc6b49cca421b44f98e41759a193 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -48,51 +48,60 @@ final class RequestsReceivedViewModel { private let groupConfirmationSubject = PassthroughSubject<Group, Never>() private let contactConfirmationSubject = PassthroughSubject<Contact, Never>() private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init()) - private var groupChats = [GroupChatInfo]() var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() init() { - Publishers.CombineLatest4( - session.groups(.pending), - session.contacts(.all), - session.groupMembers(.all), + let groupsQuery = Group.Query( + authStatus: [ + .hidden, + .pending + ]) + + let contactsQuery = Contact.Query( + authStatus: [ + .hidden, + .verified, + .verificationFailed, + .verificationInProgress + ]) + + let groupStream = session.dbManager.fetchGroupsPublisher(groupsQuery).assertNoFailure() + let contactsStream = session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure() + + Publishers.CombineLatest3( + groupStream, + contactsStream, updateSubject.eraseToAnyPublisher() ) .subscribe(on: DispatchQueue.main) .receive(on: DispatchQueue.global()) .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in - let contactRequests = data.1.filter { - $0.status == .hidden || - $0.status == .verified || - $0.status == .verificationFailed || - $0.status == .verificationInProgress - } - var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>() snapshot.appendSections([.appearing, .hidden]) - let requests = data.0.map(Request.group) + contactRequests.map(Request.contact) + let requests = data.0.map(Request.group) + data.1.map(Request.contact) let receivedRequests = requests.map { request -> RequestReceived in switch request { case let .group(group): - var leaderTitle = "" - if let leader = data.1.first(where: { $0.userId == group.leader }) { - leaderTitle = leader.nickname ?? leader.username - } else if let leader = data.2.first(where: { $0.userId == group.leader }) { - leaderTitle = leader.username + func leaderName() -> String { + if let leader = data.1.first(where: { $0.id == group.leaderId }) { + return (leader.nickname ?? leader.username) ?? "Leader is not a friend" + } else { + return "[Error retrieving leader]" + } } return RequestReceived( request: request, - isHidden: group.status == .hidden, - leader: leaderTitle + isHidden: group.authStatus == .hidden, + leader: leaderName() ) case let .contact(contact): return RequestReceived( request: request, - isHidden: contact.status == .hidden, + isHidden: contact.authStatus == .hidden, leader: nil ) } @@ -113,10 +122,6 @@ final class RequestsReceivedViewModel { receiveCompletion: { _ in }, receiveValue: { [unowned self] in itemsSubject.send($0) } ).store(in: &cancellables) - - session.groupChats(.accepted) - .sink { [unowned self] in groupChats = $0 } - .store(in: &cancellables) } func didToggleHiddenRequestsSwitcher() { @@ -137,7 +142,10 @@ final class RequestsReceivedViewModel { } func didRequestHide(group: Group) { - session.hideRequestOf(group: group) + if var group = try? session.dbManager.fetchGroups(.init(id: [group.id])).first { + group.authStatus = .hidden + _ = try? session.dbManager.saveGroup(group) + } } func didRequestAccept(group: Group) { @@ -158,52 +166,55 @@ final class RequestsReceivedViewModel { _ group: Group, _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void ) { - session.scanStrangers { [weak self] in - guard let self = self else { return } - - Publishers.CombineLatest( - self.session.contacts(.all), - self.session.groupMembers(.fromGroup(group.groupId)) - ) - .sink { (allContacts, groupMembers) in - - guard !groupMembers.map(\.status).contains(.pendingUsername) else { - completion(.failure(NSError.create(""))) // Some members are still pending username lookup... - return - } - - // Now that all members are set with their usernames lets find our friends: - // - let contactsAlsoMembers = allContacts.filter { groupMembers.map(\.userId).contains($0.userId) } - let membersNonContacts = groupMembers.filter { !contactsAlsoMembers.map(\.userId).contains($0.userId) } - - var models = [DrawerTableCellModel]() - - contactsAlsoMembers.forEach { - models.append(.init( - title: $0.nickname ?? $0.username, - image: $0.photo, - isCreator: $0.userId == group.leader, - isConnection: true - )) - } - - membersNonContacts.forEach { - models.append(.init( - title: $0.username, - image: nil, - isCreator: $0.userId == group.leader, - isConnection: false - )) - } - - completion(.success(models)) - }.store(in: &self.cancellables) - } +// session.scanStrangers { [weak self] in +// guard let self = self else { return } +// +// Publishers.CombineLatest( +// self.session.dbManager.fetchContactsPublisher(.init()).assertNoFailure(), +// self.session.dbManager.fetchGroupInfosPublisher(.init(groupId: group.id)).assertNoFailure() +// ) +// .sink { (allContacts, groupMembers) in +// +// guard !groupMembers.map(\.authStatus).contains(.pendingUsername) else { +// completion(.failure(NSError.create(""))) // Some members are still pending username lookup... +// return +// } +// +// // Now that all members are set with their usernames lets find our friends: +// // +// let contactsAlsoMembers = allContacts.filter { groupMembers.map(\.userId).contains($0.userId) } +// let membersNonContacts = groupMembers.filter { !contactsAlsoMembers.map(\.userId).contains($0.userId) } +// +// var models = [DrawerTableCellModel]() +// +// contactsAlsoMembers.forEach { +// models.append(.init( +// title: $0.nickname ?? $0.username, +// image: $0.photo, +// isCreator: $0.userId == group.leaderId, +// isConnection: true +// )) +// } +// +// membersNonContacts.forEach { +// models.append(.init( +// title: $0.username, +// image: nil, +// isCreator: $0.userId == group.leaderId, +// isConnection: false +// )) +// } +// +// completion(.success(models)) +// }.store(in: &self.cancellables) +// } } func didRequestHide(contact: Contact) { - session.hideRequestOf(contact: contact) + if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first { + contact.authStatus = .hidden + _ = try? session.dbManager.saveContact(contact) + } } func didRequestAccept(contact: Contact, nickname: String? = nil) { @@ -223,8 +234,11 @@ final class RequestsReceivedViewModel { } } - func groupChatWith(group: Group) -> GroupChatInfo { - guard let info = groupChats.first(where: { $0.group.groupId == group.groupId }) else { fatalError() } + func groupChatWith(group: Group) -> GroupInfo { + guard let info = try? session.dbManager.fetchGroupInfos(.init(groupId: group.id)).first else { + fatalError() + } + return info } } diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index ebead6be165ce126a66b729321eac1fe8135fbbb..3789428ffa0a9539e35b3344fdd820ee896d03a2 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -32,7 +32,8 @@ final class RequestsSentViewModel { var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() init() { - session.contacts(.requested) + session.dbManager.fetchContactsPublisher(.init(authStatus: [.requested])) + .assertNoFailure() .removeDuplicates() .map { data -> NSDiffableDataSourceSnapshot<Section, RequestSent> in var snapshot = NSDiffableDataSourceSnapshot<Section, RequestSent>() diff --git a/Sources/RequestsFeature/Views/RequestCell.swift b/Sources/RequestsFeature/Views/RequestCell.swift index 5a6bd6db7f9845d54745521bc9f49fd2c8dc753e..d82093630e165f487d023eca2b099f5447492a37 100644 --- a/Sources/RequestsFeature/Views/RequestCell.swift +++ b/Sources/RequestsFeature/Views/RequestCell.swift @@ -89,7 +89,7 @@ final class RequestCell: UICollectionViewCell { } setupContact( - title: contact.nickname ?? contact.username, + title: contact.nickname ?? contact.username!, photo: contact.photo, phone: phone, email: contact.email, @@ -128,7 +128,7 @@ final class RequestCell: UICollectionViewCell { } setupContact( - title: contact.nickname ?? contact.username, + title: contact.nickname ?? contact.username!, photo: contact.photo, phone: phone, email: contact.email, @@ -165,7 +165,7 @@ final class RequestCell: UICollectionViewCell { } setupContact( - title: contact.nickname ?? contact.username, + title: contact.nickname ?? contact.username!, photo: contact.photo, phone: phone, email: contact.email, diff --git a/Sources/ScanFeature/ViewModels/ScanViewModel.swift b/Sources/ScanFeature/ViewModels/ScanViewModel.swift index e91c94dad4b19f4cdfd3f408712f41df02b6705d..b940226d75390dc33a79118d44ca74ca4cbe3240 100644 --- a/Sources/ScanFeature/ViewModels/ScanViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanViewModel.swift @@ -54,10 +54,12 @@ final class ScanViewModel { return } - if let previouslyAdded = self.session.find(by: usernameAndId.0) { + + + if let previouslyAdded = try? self.session.dbManager.fetchContacts(.init(id: [usernameAndId.1])).first { var error = ScanError.unknown(Localized.Scan.Error.general) - switch previouslyAdded.status { + switch previouslyAdded.authStatus { case .friend: error = .alreadyFriends(usernameAndId.0) case .requested, .verified: @@ -71,16 +73,16 @@ final class ScanViewModel { } let contact = Contact( - photo: nil, - userId: usernameAndId.1, - email: try? self.session.extract(fact: .email, from: data), - phone: try? self.session.extract(fact: .phone, from: data), - status: .stranger, + id: usernameAndId.1, marshaled: data, username: usernameAndId.0, + email: try? self.session.extract(fact: .email, from: data), + phone: try? self.session.extract(fact: .phone, from: data), nickname: nil, - createdAt: Date(), - isRecent: false + photo: nil, + authStatus: .stranger, + isRecent: false, + createdAt: Date() ) self.succeed(with: contact) diff --git a/Sources/SearchFeature/Controllers/SearchController.swift b/Sources/SearchFeature/Controllers/SearchController.swift index 76d823020b0f4b7e5a595a3529cc6bcd3898745b..7f4250766c733d97139cd3c54d43c556adf88c7a 100644 --- a/Sources/SearchFeature/Controllers/SearchController.swift +++ b/Sources/SearchFeature/Controllers/SearchController.swift @@ -234,7 +234,7 @@ public final class SearchController: UIViewController { public func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { let contact = viewModel.itemsRelay.value[indexPath.row] - guard contact.status == .stranger else { + guard contact.authStatus == .stranger else { coordinator.toContact(contact, from: self) return } @@ -441,7 +441,7 @@ extension SearchController { ]) let drawerNicknameInput = DrawerInput( - placeholder: contact.username, + placeholder: contact.username!, validator: .init( wrongIcon: .image(Asset.sharedError.image), correctIcon: .image(Asset.sharedSuccess.image), @@ -488,7 +488,7 @@ extension SearchController { guard allowsSave else { return } drawer.dismiss(animated: true) { - self.viewModel.didSet(nickname: nickname ?? contact.username, for: contact) + self.viewModel.didSet(nickname: nickname ?? contact.username!, for: contact) } } .store(in: &drawerCancellables) diff --git a/Sources/SearchFeature/Controllers/SearchTableController.swift b/Sources/SearchFeature/Controllers/SearchTableController.swift index 625ab603defabb7d9c86b8bead13119198091e2c..a18b5856fe98c454c1a54b3ee4ca3c70d25d49dd 100644 --- a/Sources/SearchFeature/Controllers/SearchTableController.swift +++ b/Sources/SearchFeature/Controllers/SearchTableController.swift @@ -1,6 +1,7 @@ import UIKit -import Combine import Models +import Combine +import XXModels final class SearchTableController: UITableViewController { // MARK: Properties @@ -49,7 +50,7 @@ final class SearchTableController: UITableViewController { let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SearchCell.self) cell.title.text = dataSource[indexPath.row].username cell.subtitle.text = dataSource[indexPath.row].username - cell.avatar.setupProfile(title: dataSource[indexPath.row].username, image: nil, size: .large) + cell.avatar.setupProfile(title: dataSource[indexPath.row].username!, image: nil, size: .large) return cell } diff --git a/Sources/SearchFeature/ViewModels/SearchViewModel.swift b/Sources/SearchFeature/ViewModels/SearchViewModel.swift index 2ff6fccc9c6a30f3ce88935c18cc364433a4a902..7b52fb9762f47cfab9d17f2fd02383560be228bd 100644 --- a/Sources/SearchFeature/ViewModels/SearchViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchViewModel.swift @@ -168,7 +168,7 @@ final class SearchViewModel { backgroundScheduler.schedule { [weak self] in guard let self = self else { return } - self.session.update(contact) + _ = try? self.session.dbManager.saveContact(contact) } }