From c5c24bbb6320651cdb50138687104b20194f9009 Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Fri, 12 Aug 2022 00:00:39 -0300 Subject: [PATCH] Implemented filtering for banned/blocked users and reporting --- .../Controllers/SheetController.swift | 13 ++- .../Controllers/SingleChatController.swift | 83 ++++++++++++++++++- .../Helpers/CellConfigurator.swift | 5 ++ .../ViewModels/SingleChatViewModel.swift | 17 ++++ Sources/ChatFeature/Views/SheetView.swift | 40 +++++---- .../ViewModel/ChatListViewModel.swift | 6 +- .../ViewModels/ContactListViewModel.swift | 6 +- .../ViewModels/CreateGroupViewModel.swift | 2 +- .../ViewModels/MenuViewModel.swift | 2 +- .../RequestsReceivedViewModel.swift | 2 +- .../ViewModels/RequestsSentViewModel.swift | 2 +- .../ViewModels/SearchLeftViewModel.swift | 2 +- Sources/Shared/AutoGenerated/Strings.swift | 14 ++++ .../Resources/en.lproj/Localizable.strings | 15 ++++ 14 files changed, 180 insertions(+), 29 deletions(-) diff --git a/Sources/ChatFeature/Controllers/SheetController.swift b/Sources/ChatFeature/Controllers/SheetController.swift index f0f4fde8..974f086d 100644 --- a/Sources/ChatFeature/Controllers/SheetController.swift +++ b/Sources/ChatFeature/Controllers/SheetController.swift @@ -5,6 +5,7 @@ final class SheetController: UIViewController { enum Action { case clear case details + case report } lazy private var screenView = SheetView() @@ -23,7 +24,7 @@ final class SheetController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - screenView.clear + screenView.clearButton .publisher(for: .touchUpInside) .sink { [unowned self] in dismiss(animated: true) { [weak actionRelay] in @@ -31,12 +32,20 @@ final class SheetController: UIViewController { } }.store(in: &cancellables) - screenView.details + screenView.detailsButton .publisher(for: .touchUpInside) .sink { [unowned self] in dismiss(animated: true) { [weak actionRelay] in actionRelay?.send(.details) } }.store(in: &cancellables) + + screenView.reportButton + .publisher(for: .touchUpInside) + .sink { [unowned self] in + dismiss(animated: true) { [weak actionRelay] in + actionRelay?.send(.report) + } + }.store(in: &cancellables) } } diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index d1d7785a..81fa1c7c 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -187,6 +187,7 @@ public final class SingleChatController: UIViewController { navigationItem.rightBarButtonItem = UIBarButtonItem(customView: moreButton) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: infoView) + navigationItem.leftItemsSupplementBackButton = true } private func setupInputController() { @@ -249,6 +250,8 @@ public final class SingleChatController: UIViewController { presentDeleteAllDrawer() case .details: coordinator.toContact(viewModel.contact, from: self) + case .report: + presentReportDrawer() } }.store(in: &cancellables) @@ -263,6 +266,12 @@ public final class SingleChatController: UIViewController { } }.store(in: &cancellables) + viewModel.reportPopupPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + presentReportDrawer() + }.store(in: &cancellables) + viewModel.isOnline .removeDuplicates() .receive(on: DispatchQueue.main) @@ -383,6 +392,77 @@ public final class SingleChatController: UIViewController { return drawer } + private func presentReportDrawer() { + let cancelButton = CapsuleButton() + cancelButton.setStyle(.seeThrough) + cancelButton.setTitle(Localized.Chat.Report.cancel, for: .normal) + + let reportButton = CapsuleButton() + reportButton.setStyle(.red) + reportButton.setTitle(Localized.Chat.Report.action, for: .normal) + + let drawer = DrawerController(with: [ + DrawerImage( + image: Asset.drawerNegative.image + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 18.0), + text: Localized.Chat.Report.title, + color: Asset.neutralActive.color + ), + DrawerText( + font: Fonts.Mulish.semiBold.font(size: 14.0), + text: Localized.Chat.Report.subtitle, + color: Asset.neutralWeak.color, + lineHeightMultiple: 1.35, + spacingAfter: 25 + ), + DrawerStack( + axis: .vertical, + spacing: 20.0, + views: [reportButton, cancelButton] + ) + ]) + + reportButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { + drawer.dismiss(animated: true) { [weak self] in + guard let self = self else { return } + self.drawerCancellables.removeAll() + self.didProceedWithReport() + } + }.store(in: &drawerCancellables) + + cancelButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { + drawer.dismiss(animated: true) { [weak self] in + self?.drawerCancellables.removeAll() + } + }.store(in: &drawerCancellables) + + coordinator.toDrawer(drawer, from: self) + } + + private func didProceedWithReport() { + var screenshotImage: UIImage? + + let layer = UIApplication.shared.keyWindow!.layer + + let scale = UIScreen.main.scale + UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale); + guard let context = UIGraphicsGetCurrentContext() else { return } + layer.render(in: context) + + if let image = UIGraphicsGetImageFromCurrentImageContext() { + UIGraphicsEndImageContext() + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + viewModel.uploadReport(screenshot: image) + navigationController?.popViewController(animated: true) + } + } + private func presentDeleteAllDrawer() { let clearButton = CapsuleButton() clearButton.setStyle(.red) @@ -632,7 +712,8 @@ extension SingleChatController: UICollectionViewDelegate { ActionFactory.build(from: item, action: .copy, closure: self.viewModel.didRequestCopy(_:)), ActionFactory.build(from: item, action: .retry, closure: self.viewModel.didRequestRetry(_:)), ActionFactory.build(from: item, action: .reply, closure: self.viewModel.didRequestReply(_:)), - ActionFactory.build(from: item, action: .delete, closure: self.viewModel.didRequestDeleteSingle(_:)) + ActionFactory.build(from: item, action: .delete, closure: self.viewModel.didRequestDeleteSingle(_:)), + ActionFactory.build(from: item, action: .report, closure: self.viewModel.didRequestReport(_:)) ].compactMap { $0 }) } } diff --git a/Sources/ChatFeature/Helpers/CellConfigurator.swift b/Sources/ChatFeature/Helpers/CellConfigurator.swift index 4f0ad3ca..d9b61fd9 100644 --- a/Sources/ChatFeature/Helpers/CellConfigurator.swift +++ b/Sources/ChatFeature/Helpers/CellConfigurator.swift @@ -399,6 +399,7 @@ struct ActionFactory { case retry case reply case delete + case report var title: String { switch self { @@ -411,6 +412,8 @@ struct ActionFactory { return Localized.Chat.BubbleMenu.reply case .delete: return Localized.Chat.BubbleMenu.delete + case .report: + return Localized.Chat.BubbleMenu.report } } } @@ -422,6 +425,8 @@ struct ActionFactory { ) -> UIAction? { switch action { + case .report: + guard item.status == .received else { return nil } case .reply: guard item.status == .received || item.status == .sent else { return nil } case .retry: diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index f51a8936..cf9404a3 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -34,6 +34,7 @@ final class SingleChatViewModel { private let replySubject = PassthroughSubject<(String, String), Never>() private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) + private let reportPopupSubject = PassthroughSubject<Void, Never>() var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) @@ -44,6 +45,10 @@ final class SingleChatViewModel { var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } + var reportPopupPublisher: AnyPublisher<Void, Never> { + reportPopupSubject.eraseToAnyPublisher() + } + var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> { sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in var snapshot = [ArraySection<ChatSection, Message>]() @@ -172,6 +177,10 @@ final class SingleChatViewModel { didRequestDelete([model]) } + func didRequestReport(_: Message) { + reportPopupSubject.send() + } + func abortReply() { stagedReply = nil } @@ -211,6 +220,14 @@ final class SingleChatViewModel { return (contactTitle, message.text) } + func uploadReport(screenshot: UIImage) { + UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil) + + var contact = contact + contact.isBlocked = true + _ = try? session.dbManager.saveContact(contact) + } + func showRoundFrom(_ roundURL: String?) { if let urlString = roundURL, !urlString.isEmpty { navigationRoutes.send(.webview(urlString)) diff --git a/Sources/ChatFeature/Views/SheetView.swift b/Sources/ChatFeature/Views/SheetView.swift index 86459e76..a4cdfeb1 100644 --- a/Sources/ChatFeature/Views/SheetView.swift +++ b/Sources/ChatFeature/Views/SheetView.swift @@ -2,9 +2,10 @@ import UIKit import Shared final class SheetView: UIView { - let stack = UIStackView() - let clear = SheetButton() - let details = SheetButton() + let stackView = UIStackView() + let clearButton = SheetButton() + let reportButton = SheetButton() + let detailsButton = SheetButton() init() { super.init(frame: .zero) @@ -13,23 +14,28 @@ final class SheetView: UIView { layer.masksToBounds = true backgroundColor = Asset.neutralWhite.color - clear.image.image = Asset.chatListDeleteSwipe.image - clear.title.text = Localized.Chat.SheetMenu.clear + clearButton.image.image = Asset.chatListDeleteSwipe.image + clearButton.title.text = Localized.Chat.SheetMenu.clear - details.tintColor = Asset.neutralDark.color - details.image.image = Asset.searchUsername.image - details.title.text = Localized.Chat.SheetMenu.details + detailsButton.tintColor = Asset.neutralDark.color + detailsButton.image.image = Asset.searchUsername.image + detailsButton.title.text = Localized.Chat.SheetMenu.details - stack.axis = .vertical - stack.distribution = .fillEqually - stack.addArrangedSubview(clear) - stack.addArrangedSubview(details) - addSubview(stack) + reportButton.tintColor = Asset.accentDanger.color + reportButton.image.image = Asset.searchUsername.image + reportButton.title.text = Localized.Chat.SheetMenu.report - stack.snp.makeConstraints { make in - make.top.equalToSuperview().offset(25) - make.left.right.equalToSuperview() - make.bottom.equalTo(safeAreaLayoutGuide) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.addArrangedSubview(clearButton) + stackView.addArrangedSubview(detailsButton) + stackView.addArrangedSubview(reportButton) + addSubview(stackView) + + stackView.snp.makeConstraints { + $0.top.equalToSuperview().offset(25) + $0.left.right.equalToSuperview() + $0.bottom.equalTo(safeAreaLayoutGuide) } } diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index f481b96f..efd5e463 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -107,7 +107,7 @@ final class ChatListViewModel { .confirmationFailed, .verificationFailed, .verificationInProgress - ]) + ], isBlocked: false, isBanned: false) return Publishers.CombineLatest( session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), @@ -127,7 +127,9 @@ final class ChatListViewModel { ChatInfo.Query( contactChatInfoQuery: .init( userId: session.myId, - authStatus: [.friend] + authStatus: [.friend], + isBlocked: false, + isBanned: false ), groupChatInfoQuery: GroupChatInfo.Query( authStatus: [.participating] diff --git a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift index 04674460..f3fea877 100644 --- a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift @@ -8,7 +8,9 @@ final class ContactListViewModel { @Dependency private var session: SessionType var contacts: AnyPublisher<[Contact], Never> { - session.dbManager.fetchContactsPublisher(.init(authStatus: [.friend])) + let query = Contact.Query(authStatus: [.friend], isBlocked: false, isBanned: false) + + return session.dbManager.fetchContactsPublisher(query) .assertNoFailure() .map { $0.filter { $0.id != self.session.myId }} .eraseToAnyPublisher() @@ -22,7 +24,7 @@ final class ContactListViewModel { .confirmationFailed, .verificationFailed, .verificationInProgress - ]) + ], isBlocked: false, isBanned: false) return Publishers.CombineLatest( session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), diff --git a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift index a8f94de8..668b396b 100644 --- a/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/CreateGroupViewModel.swift @@ -42,7 +42,7 @@ final class CreateGroupViewModel { // MARK: Lifecycle init() { - session.dbManager.fetchContactsPublisher(.init(authStatus: [.friend])) + session.dbManager.fetchContactsPublisher(.init(authStatus: [.friend], isBlocked: false, isBanned: false)) .assertNoFailure() .map { $0.filter { $0.id != self.session.myId }} .map { $0.sorted(by: { $0.username! < $1.username! })} diff --git a/Sources/MenuFeature/ViewModels/MenuViewModel.swift b/Sources/MenuFeature/ViewModels/MenuViewModel.swift index b4ebd757..4d8b0db9 100644 --- a/Sources/MenuFeature/ViewModels/MenuViewModel.swift +++ b/Sources/MenuFeature/ViewModels/MenuViewModel.swift @@ -19,7 +19,7 @@ final class MenuViewModel { .confirmationFailed, .verificationFailed, .verificationInProgress - ]) + ], isBlocked: false, isBanned: false) return Publishers.CombineLatest( session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index ded18f77..57efb1ea 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -65,7 +65,7 @@ final class RequestsReceivedViewModel { .verified, .verificationFailed, .verificationInProgress - ]) + ], isBlocked: false, isBanned: false) let groupStream = session.dbManager.fetchGroupsPublisher(groupsQuery).assertNoFailure() let contactsStream = session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure() diff --git a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift index f94ed8f5..26a6f485 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsSentViewModel.swift @@ -36,7 +36,7 @@ final class RequestsSentViewModel { let query = Contact.Query(authStatus: [ .requested, .requesting - ]) + ], isBlocked: false, isBanned: false) session.dbManager.fetchContactsPublisher(query) .assertNoFailure() diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index 9f9d0ffd..7d49d19e 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -144,7 +144,7 @@ final class SearchLeftViewModel { var snapshot = SearchSnapshot() if var user = user { - if let contact = try? session.dbManager.fetchContacts(.init(id: [user.id])).first { + if let contact = try? session.dbManager.fetchContacts(.init(id: [user.id], isBlocked: false, isBanned: false)).first { user.authStatus = contact.authStatus } diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index 03ada00d..ee37eb48 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -324,6 +324,8 @@ public enum Localized { public static let delete = Localized.tr("Localizable", "chat.bubbleMenu.delete") /// Reply public static let reply = Localized.tr("Localizable", "chat.bubbleMenu.reply") + /// Report + public static let report = Localized.tr("Localizable", "chat.bubbleMenu.report") /// Retry public static let retry = Localized.tr("Localizable", "chat.bubbleMenu.retry") /// Select @@ -351,6 +353,16 @@ public enum Localized { /// All public static let deleteAll = Localized.tr("Localizable", "chat.menu.deleteAll") } + public enum Report { + /// Confirm and Report + public static let action = Localized.tr("Localizable", "chat.report.action") + /// Cancel + public static let cancel = Localized.tr("Localizable", "chat.report.cancel") + /// Reporting this user will block them, delete them from your connections and you won’t see direct messages from them again. In case this user is marked as banned user by us you won’t also see any new group chat msgs from this user + public static let subtitle = Localized.tr("Localizable", "chat.report.subtitle") + /// Report user + public static let title = Localized.tr("Localizable", "chat.report.title") + } public enum RetrySheet { /// Cancel public static let cancel = Localized.tr("Localizable", "chat.retrySheet.cancel") @@ -370,6 +382,8 @@ public enum Localized { public static let clear = Localized.tr("Localizable", "chat.sheetMenu.clear") /// View contact profile public static let details = Localized.tr("Localizable", "chat.sheetMenu.details") + /// Report user + public static let report = Localized.tr("Localizable", "chat.sheetMenu.report") } } diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 033e7a6f..5d910155 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -114,6 +114,8 @@ = "Select"; "chat.bubbleMenu.retry" = "Retry"; +"chat.bubbleMenu.report" += "Report"; "chat.e2e.placeholder" = "You and %@ now have a #quantum-secure#, completely private channel for messaging.\n#Say hello#!"; @@ -125,6 +127,8 @@ = "Clear chat"; "chat.sheetMenu.details" = "View contact profile"; +"chat.sheetMenu.report" += "Report user"; "chat.retrySheet.retry" = "Try again"; "chat.retrySheet.delete" @@ -174,6 +178,17 @@ "chat.clear.cancel" = "Cancel"; +// ChatFeature - Report + +"chat.report.title" += "Report user"; +"chat.report.subtitle" += "Reporting this user will block them, delete them from your connections and you won’t see direct messages from them again. In case this user is marked as banned user by us you won’t also see any new group chat msgs from this user"; +"chat.report.action" += "Confirm and Report"; +"chat.report.cancel" += "Cancel"; + // ScanFeature "scan.status.reading" -- GitLab