Signal-iOS/Signal/src/ViewControllers/ConversationView/ConversationViewController+MessageRequest.swift
2021-09-03 11:41:34 -07:00

424 lines
20 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SafariServices
@objc
extension ConversationViewController: MessageRequestDelegate {
func messageRequestViewDidTapBlock(mode: MessageRequestMode) {
AssertIsOnMainThread()
switch mode {
case .none:
owsFailDebug("Invalid mode.")
case .contactOrGroupRequest:
let blockSheet = createBlockThreadActionSheet()
presentActionSheet(blockSheet)
case .groupInviteRequest:
showBlockInviteActionSheet()
}
}
func showBlockInviteActionSheet() {
Logger.info("")
guard let groupThread = thread as? TSGroupThread else {
owsFailDebug("Invalid thread.")
return
}
guard let localAddress = tsAccountManager.localAddress else {
owsFailDebug("missing local address")
return
}
let groupMembership = groupThread.groupModel.groupMembership
guard groupMembership.isInvitedMember(localAddress) else {
owsFailDebug("Can't reject invite if not pending.")
return
}
guard let addedByUuid = groupMembership.addedByUuid(forInvitedMember: localAddress) else {
owsFailDebug("Missing addedByUuid.")
return
}
let addedByAddress = SignalServiceAddress(uuid: addedByUuid)
let addedByName = contactsManager.displayName(for: addedByAddress)
let actionSheet = ActionSheetController(title: nil, message: nil)
actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("GROUPS_INVITE_BLOCK_GROUP",
comment: "Label for 'block group' button in group invite view."),
style: .default) { [weak self] _ in
self?.blockThread()
})
let blockInviterTitle = String(format: NSLocalizedString("GROUPS_INVITE_BLOCK_INVITER_FORMAT",
comment: "Label for 'block inviter' button in group invite view. Embeds {{name of user who invited you}}."),
addedByName)
actionSheet.addAction(ActionSheetAction(title: blockInviterTitle,
style: .default) { [weak self] _ in
self?.blockUserAndDelete(addedByAddress)
})
let blockGroupAndInviterTitle = String(format: NSLocalizedString("GROUPS_INVITE_BLOCK_GROUP_AND_INVITER_FORMAT",
comment: "Label for 'block group and inviter' button in group invite view. Embeds {{name of user who invited you}}."),
addedByName)
actionSheet.addAction(ActionSheetAction(title: blockGroupAndInviterTitle,
style: .default) { [weak self] _ in
self?.blockUserAndGroupAndDelete(addedByAddress)
})
actionSheet.addAction(OWSActionSheets.cancelAction)
presentActionSheet(actionSheet)
}
func blockThread() {
// Leave the group while blocking the thread.
databaseStorage.write { transaction in
blockingManager.addBlockedThread(thread,
blockMode: .localShouldLeaveGroups,
transaction: transaction)
}
syncManager.sendMessageRequestResponseSyncMessage(thread: thread,
responseType: .block)
}
func blockThreadAndDelete() {
// Do not leave the group while blocking the thread; we'll
// that below so that we can surface an error to the user
// if leaving the group fails.
databaseStorage.write { transaction in
blockingManager.addBlockedThread(thread,
blockMode: .localShouldNotLeaveGroups,
transaction: transaction)
}
syncManager.sendMessageRequestResponseSyncMessage(thread: self.thread,
responseType: .blockAndDelete)
leaveAndSoftDeleteThread()
}
func blockThreadAndReportSpam() {
databaseStorage.write { transaction in
blockingManager.addBlockedThread(thread,
blockMode: .localShouldNotLeaveGroups,
transaction: transaction)
}
syncManager.sendMessageRequestResponseSyncMessage(thread: self.thread, responseType: .block)
reportSpam()
presentToast(
text: NSLocalizedString(
"MESSAGE_REQUEST_SPAM_REPORTED_AND_BLOCKED",
comment: "String indicating that spam has been reported and the chat has been blocked."
),
extraVInset: bottomBar.height
)
}
func reportSpam() {
guard let contactThread = thread as? TSContactThread else {
return owsFailDebug("Unexpected thread type for reporting spam \(type(of: thread))")
}
guard let senderPhoneNumber = contactThread.contactAddress.phoneNumber else {
return owsFailDebug("Missing phone number for reporting spam from \(contactThread.contactAddress)")
}
// We only report a selection of the N most recent messages
// in the conversation.
let maxMessagesToReport = 3
let guidsToReport: [String] = databaseStorage.read { transaction in
var guidsToReport = [String]()
do {
try InteractionFinder(
threadUniqueId: self.thread.uniqueId
).enumerateRecentInteractions(
transaction: transaction
) { interaction, stop in
guard let incomingMessage = interaction as? TSIncomingMessage else { return }
if let serverGuid = incomingMessage.serverGuid {
guidsToReport.append(serverGuid)
}
guard guidsToReport.count < maxMessagesToReport else {
stop.pointee = true
return
}
}
} catch {
owsFailDebug("Failed to lookup guids to report \(error)")
}
return guidsToReport
}
guard !guidsToReport.isEmpty else {
Logger.warn("No messages with serverGuids to report.")
return
}
Logger.info("Reporting \(guidsToReport.count) message(s) from \(senderPhoneNumber) as spam.")
var promises = [Promise<Void>]()
for guid in guidsToReport {
let request = OWSRequestFactory.reportSpam(fromPhoneNumber: senderPhoneNumber, withServerGuid: guid)
promises.append(networkManager.makePromise(request: request).asVoid())
}
Promise.when(fulfilled: promises).done {
Logger.info("Successfully reported \(guidsToReport.count) message(s) from \(senderPhoneNumber) as spam.")
}.catch { error in
owsFailDebug("Failed to report message(s) from \(senderPhoneNumber) as spam with error: \(error)")
}
}
func blockUserAndDelete(_ address: SignalServiceAddress) {
// Do not leave the group while blocking the thread; we'll
// that below so that we can surface an error to the user
// if leaving the group fails.
databaseStorage.write { transaction in
blockingManager.addBlockedAddress(address,
blockMode: .localShouldNotLeaveGroups,
transaction: transaction)
}
syncManager.sendMessageRequestResponseSyncMessage(thread: self.thread,
responseType: .delete)
leaveAndSoftDeleteThread()
}
func blockUserAndGroupAndDelete(_ address: SignalServiceAddress) {
ConversationViewController.databaseStorage.write { transaction in
if let groupThread = self.thread as? TSGroupThread {
// Do not leave the group while blocking the thread; we'll
// that below so that we can surface an error to the user
// if leaving the group fails.
self.blockingManager.addBlockedGroup(groupModel: groupThread.groupModel,
blockMode: .localShouldNotLeaveGroups,
transaction: transaction)
} else {
owsFailDebug("Invalid thread.")
}
self.blockingManager.addBlockedAddress(address, blockMode: .localShouldNotLeaveGroups, transaction: transaction)
}
syncManager.sendMessageRequestResponseSyncMessage(thread: self.thread,
responseType: .blockAndDelete)
leaveAndSoftDeleteThread()
}
func messageRequestViewDidTapDelete() {
AssertIsOnMainThread()
let deleteSheet = createDeleteThreadActionSheet()
presentActionSheet(deleteSheet)
}
func leaveAndSoftDeleteThread() {
AssertIsOnMainThread()
let completion = {
ConversationViewController.databaseStorage.write { transaction in
self.thread.softDelete(with: transaction)
}
self.conversationSplitViewController?.closeSelectedConversation(animated: true)
}
guard let groupThread = thread as? TSGroupThread,
groupThread.isLocalUserFullOrInvitedMember else {
// If we don't need to leave the group, finish up immediately.
return completion()
}
// Leave the group if we're a member.
GroupManager.leaveGroupOrDeclineInviteAsyncWithUI(groupThread: groupThread, fromViewController: self, success: completion)
}
func messageRequestViewDidTapAccept(mode: MessageRequestMode) {
messageRequestViewDidTapAccept(mode: mode, unblockThread: false)
}
func messageRequestViewDidTapAccept(mode: MessageRequestMode, unblockThread: Bool) {
AssertIsOnMainThread()
let thread = self.thread
let completion = {
SDSDatabaseStorage.shared.asyncWrite { transaction in
if unblockThread {
self.blockingManager.removeBlockedThread(thread, wasLocallyInitiated: true, transaction: transaction)
}
// Whitelist the thread
self.profileManager.addThread(toProfileWhitelist: thread, transaction: transaction)
// Send a sync message notifying our other devices the request was accepted
self.syncManager.sendMessageRequestResponseSyncMessage(
thread: thread,
responseType: .accept,
transaction: transaction
)
// Send our profile key to the sender
let profileKeyMessage = OWSProfileKeyMessage(thread: thread)
Self.messageSenderJobQueue.add(message: profileKeyMessage.asPreparer, transaction: transaction)
}
}
switch mode {
case .none:
owsFailDebug("Invalid mode.")
case .contactOrGroupRequest:
completion()
case .groupInviteRequest:
guard let groupThread = thread as? TSGroupThread else {
owsFailDebug("Invalid thread.")
return
}
GroupManager.acceptGroupInviteAsync(groupThread, fromViewController: self, success: completion)
}
}
func messageRequestViewDidTapUnblock(mode: MessageRequestMode) {
AssertIsOnMainThread()
let threadName: String
let message: String
if let groupThread = thread as? TSGroupThread {
threadName = groupThread.groupNameOrDefault
message = NSLocalizedString(
"BLOCK_LIST_UNBLOCK_GROUP_MESSAGE", comment: "An explanation of what unblocking a group means.")
} else if let contactThread = thread as? TSContactThread {
threadName = contactsManager.displayName(for: contactThread.contactAddress)
message = NSLocalizedString(
"BLOCK_LIST_UNBLOCK_CONTACT_MESSAGE", comment: "An explanation of what unblocking a contact means.")
} else {
owsFailDebug("Invalid thread.")
return
}
let title = String(format: NSLocalizedString("BLOCK_LIST_UNBLOCK_TITLE_FORMAT",
comment: "A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}."),
threadName)
OWSActionSheets.showConfirmationAlert(
title: title,
message: message,
proceedTitle: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON",
comment: "Button label for the 'unblock' button")
) { _ in
self.messageRequestViewDidTapAccept(mode: mode, unblockThread: true)
}
}
func messageRequestViewDidTapLearnMore() {
AssertIsOnMainThread()
// TODO Message Request: Use right support url. Right now this just links to the profiles FAQ
guard let url = URL(string: "https://support.signal.org/hc/articles/360007459591") else {
return owsFailDebug("Invalid url.")
}
let safariVC = SFSafariViewController(url: url)
present(safariVC, animated: true)
}
}
extension ConversationViewController: NameCollisionResolutionDelegate {
func createBlockThreadActionSheet(sheetCompletion: ((Bool) -> Void)? = nil) -> ActionSheetController {
Logger.info("")
let actionSheetTitleFormat: String
let actionSheetMessage: String
if thread.isGroupThread {
actionSheetTitleFormat = NSLocalizedString("MESSAGE_REQUEST_BLOCK_GROUP_TITLE_FORMAT",
comment: "Action sheet title to confirm blocking a group via a message request. Embeds {{group name}}")
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_BLOCK_GROUP_MESSAGE",
comment: "Action sheet message to confirm blocking a group via a message request.")
} else {
actionSheetTitleFormat = NSLocalizedString("MESSAGE_REQUEST_BLOCK_CONVERSATION_TITLE_FORMAT",
comment: "Action sheet title to confirm blocking a contact via a message request. Embeds {{contact name or phone number}}")
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_BLOCK_CONVERSATION_MESSAGE",
comment: "Action sheet message to confirm blocking a conversation via a message request.")
}
let threadName = contactsManager.displayNameWithSneakyTransaction(thread: thread)
let actionSheetTitle = String(format: actionSheetTitleFormat, threadName)
let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
let blockActionTitle = NSLocalizedString("MESSAGE_REQUEST_BLOCK_ACTION",
comment: "Action sheet action to confirm blocking a thread via a message request.")
let blockAndDeleteActionTitle = NSLocalizedString("MESSAGE_REQUEST_BLOCK_AND_DELETE_ACTION",
comment: "Action sheet action to confirm blocking and deleting a thread via a message request.")
let blockAndReportSpamActionTitle = NSLocalizedString("MESSAGE_REQUEST_BLOCK_AND_REPORT_SPAM_ACTION",
comment: "Action sheet action to confirm blocking and report spam for a thread via a message request.")
actionSheet.addAction(ActionSheetAction(title: blockActionTitle) { [weak self] _ in
self?.blockThread()
sheetCompletion?(true)
})
if thread.isGroupThread {
actionSheet.addAction(ActionSheetAction(title: blockAndDeleteActionTitle) { [weak self] _ in
self?.blockThreadAndDelete()
sheetCompletion?(true)
})
} else {
actionSheet.addAction(ActionSheetAction(title: blockAndReportSpamActionTitle) { [weak self] _ in
self?.blockThreadAndReportSpam()
sheetCompletion?(true)
})
}
actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel, handler: { _ in
sheetCompletion?(false)
}))
return actionSheet
}
func createDeleteThreadActionSheet(sheetCompletion: ((Bool) -> Void)? = nil) -> ActionSheetController {
let actionSheetTitle: String
let actionSheetMessage: String
let confirmationText: String
var isMemberOfGroup = false
if let groupThread = thread as? TSGroupThread {
isMemberOfGroup = groupThread.isLocalUserMemberOfAnyKind
}
if isMemberOfGroup {
actionSheetTitle = NSLocalizedString("MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_TITLE",
comment: "Action sheet title to confirm deleting a group via a message request.")
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_MESSAGE",
comment: "Action sheet message to confirm deleting a group via a message request.")
confirmationText = NSLocalizedString("MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_ACTION",
comment: "Action sheet action to confirm deleting a group via a message request.")
} else { // either 1:1 thread, or a group of which I'm not a member
actionSheetTitle = NSLocalizedString("MESSAGE_REQUEST_DELETE_CONVERSATION_TITLE",
comment: "Action sheet title to confirm deleting a conversation via a message request.")
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_DELETE_CONVERSATION_MESSAGE",
comment: "Action sheet message to confirm deleting a conversation via a message request.")
confirmationText = NSLocalizedString("MESSAGE_REQUEST_DELETE_CONVERSATION_ACTION",
comment: "Action sheet action to confirm deleting a conversation via a message request.")
}
let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
actionSheet.addAction(ActionSheetAction(title: confirmationText, handler: { _ in
self.syncManager.sendMessageRequestResponseSyncMessage(thread: self.thread,
responseType: .delete)
self.leaveAndSoftDeleteThread()
sheetCompletion?(true)
}))
actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel, handler: { _ in
sheetCompletion?(false)
}))
return actionSheet
}
func nameCollisionControllerDidComplete(_ controller: NameCollisionResolutionViewController, dismissConversationView: Bool) {
if dismissConversationView {
// This may have already been closed (e.g. if the user requested deletion), but
// it's not guaranteed (e.g. the user blocked the request). Let's close it just
// to be safe.
self.conversationSplitViewController?.closeSelectedConversation(animated: false)
} else {
// Conversation view is being kept around. Update the banner state to account for any changes
ensureBannerState()
}
controller.dismiss(animated: true, completion: nil)
}
}