Commit 943c9293 authored by Andreas Traczyk's avatar Andreas Traczyk Committed by Kateryna Kostiuk

contacts: add the ability to block users via the smartlist and conversation

- Adds the ability to block an existing contact. The feature can
  be accessed via the smartlist swipe actions, or a top bar button
  within the conversation. A confirmation alert will be presented
  to the user.

- Adds a confirmation alert to the delete conversation action.

Change-Id: Ib7373a6434d280115ad08c5229c13cfa0f1ae7c3
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent da4a18d4
......@@ -49,6 +49,7 @@ enum Asset {
static let accountIcon = ImageAsset(name: "account_icon")
static let addPerson = ImageAsset(name: "add_person")
static let backgroundRing = ImageAsset(name: "background_ring")
static let blockIcon = ImageAsset(name: "block_icon")
static let contactRequestIcon = ImageAsset(name: "contact_request_icon")
static let conversationIcon = ImageAsset(name: "conversation_icon")
static let device = ImageAsset(name: "device")
......@@ -64,6 +65,7 @@ enum Asset {
accountIcon,
addPerson,
backgroundRing,
blockIcon,
contactRequestIcon,
conversationIcon,
device,
......
......@@ -13,6 +13,15 @@ enum L10n {
static let devicesListHeader = L10n.tr("Localizable", "accountPage.devicesListHeader")
}
enum Actions {
/// Block
static let blockAction = L10n.tr("Localizable", "actions.blockAction")
/// Cancel
static let cancelAction = L10n.tr("Localizable", "actions.cancelAction")
/// Delete
static let deleteAction = L10n.tr("Localizable", "actions.deleteAction")
}
enum Alerts {
/// Account Added
static let accountAddedTitle = L10n.tr("Localizable", "alerts.accountAddedTitle")
......@@ -30,6 +39,14 @@ enum L10n {
static let accountNoNetworkMessage = L10n.tr("Localizable", "alerts.accountNoNetworkMessage")
/// Can't connect to the network
static let accountNoNetworkTitle = L10n.tr("Localizable", "alerts.accountNoNetworkTitle")
/// Are you sure you want to block this contact? The conversation history with this contact will also be deleted permanently.
static let confirmBlockContact = L10n.tr("Localizable", "alerts.confirmBlockContact")
/// Block Contact
static let confirmBlockContactTitle = L10n.tr("Localizable", "alerts.confirmBlockContactTitle")
/// Are you sure you want to delete this conversation permanently?
static let confirmDeleteConversation = L10n.tr("Localizable", "alerts.confirmDeleteConversation")
/// Delete Conversation
static let confirmDeleteConversationTitle = L10n.tr("Localizable", "alerts.confirmDeleteConversationTitle")
/// Please close application and try to open it again
static let dbFailedMessage = L10n.tr("Localizable", "alerts.dbFailedMessage")
/// An error happned when launching Ring
......
......@@ -92,16 +92,15 @@ class ContactRequestsViewModel: Stateable, ViewModel {
func discard(withItem item: ContactRequestItem) -> Observable<Void> {
return self.contactsService.discard(contactRequest: item.contactRequest,
withAccount: self.accountsService.currentAccount!)
withAccountId: item.contactRequest.accountId)
}
func ban(withItem item: ContactRequestItem) -> Observable<Void> {
let discardCompleted = self.contactsService.discard(contactRequest: item.contactRequest,
withAccount: self.accountsService.currentAccount!)
withAccountId: item.contactRequest.accountId)
let removeCompleted = self.contactsService.removeContact(withRingId: item.contactRequest.ringId,
ban: true,
withAccount: self.accountsService.currentAccount!)
withAccountId: item.contactRequest.accountId)
return Observable<Void>.zip(discardCompleted, removeCompleted) { _, _ in
return
......
......@@ -115,15 +115,52 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
self.inviteItemTapped()
}).disposed(by: self.disposeBag)
self.navigationItem.rightBarButtonItem = inviteItem
self.viewModel.inviteButtonIsAvailable.asObservable().bind(to: inviteItem.rx.isEnabled).disposed(by: disposeBag)
//block contact button
let blockItem = UIBarButtonItem()
blockItem.image = UIImage(named: "block_icon")
blockItem.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] in
self.blockItemTapped()
}).disposed(by: self.disposeBag)
self.navigationItem.rightBarButtonItems = [blockItem, inviteItem]
Observable<[UIBarButtonItem]>
.combineLatest(self.viewModel.inviteButtonIsAvailable.asObservable(),
self.viewModel.blockButtonIsAvailable.asObservable(),
resultSelector: { inviteButton, blockButton in
var buttons = [UIBarButtonItem]()
if blockButton {
buttons.append(blockItem)
}
if inviteButton {
buttons.append(inviteItem)
}
return buttons
})
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] buttons in
self?.navigationItem.rightBarButtonItems = buttons
}).disposed(by: self.disposeBag)
}
func inviteItemTapped() {
self.viewModel?.sendContactRequest()
}
func blockItemTapped() {
let alert = UIAlertController(title: L10n.Alerts.confirmBlockContactTitle, message: L10n.Alerts.confirmBlockContact, preferredStyle: .alert)
let blockAction = UIAlertAction(title: L10n.Actions.blockAction, style: .destructive) { (_: UIAlertAction!) -> Void in
self.viewModel.block()
}
let cancelAction = UIAlertAction(title: L10n.Actions.cancelAction, style: .default) { (_: UIAlertAction!) -> Void in }
alert.addAction(blockAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
......
......@@ -91,23 +91,28 @@ class ConversationViewModel: ViewModel {
})
.disposed(by: self.disposeBag)
// invite and block buttons
if let contact = contact {
self.inviteButtonIsAvailable.onNext(!contact.confirmed)
let showInviteButton = !contact.confirmed && !contact.banned
self.inviteButtonIsAvailable.onNext(showInviteButton)
self.blockButtonIsAvailable.onNext(!contact.banned)
}
self.contactsService.contactStatus.filter({ cont in
return cont.ringId == contactRingId
})
.subscribe(onNext: { [unowned self] cont in
self.inviteButtonIsAvailable.onNext(!cont.confirmed)
.subscribe(onNext: { [unowned self] contact in
let showInviteButton = !contact.confirmed && !contact.banned
self.inviteButtonIsAvailable.onNext(showInviteButton)
let isContact = self.contactsService.contact(withRingId: contact.ringId) != nil && !contact.banned
self.blockButtonIsAvailable.onNext(isContact)
}).disposed(by: self.disposeBag)
// subscribe to presence updates for the conversation's associated contact
if let contactPresence = self.presenceService.contactPresence[contactRingId] {
self.contactPresence.value = contactPresence
} else {
self.log.warning("Contact presence unkown for: \(contactRingId)")
self.log.warning("Contact presence unknown for: \(contactRingId)")
self.contactPresence.value = false
}
self.presenceService
......@@ -162,6 +167,8 @@ class ConversationViewModel: ViewModel {
var inviteButtonIsAvailable = BehaviorSubject(value: true)
var blockButtonIsAvailable = BehaviorSubject(value: false)
var contactPresence = Variable<Bool>(false)
var unreadMessages: String {
......@@ -262,6 +269,11 @@ class ConversationViewModel: ViewModel {
}
func sendContactRequest() {
if let contact = self.contactsService
.contact(withRingId: self.conversation.value.recipientRingId),
contact.banned {
return
}
VCardUtils.loadVCard(named: VCardFiles.myProfile.rawValue,
inFolder: VCardFolders.profile.rawValue)
.subscribe(onSuccess: { [unowned self] (card) in
......@@ -281,4 +293,40 @@ class ConversationViewModel: ViewModel {
}.disposed(by: self.disposeBag)
}
func block() {
let contactRingId = self.conversation.value.recipientRingId
let accountId = self.conversation.value.accountId
var blockComplete: Observable<Void>
let removeCompleted = self.contactsService.removeContact(withRingId: contactRingId,
ban: true,
withAccountId: accountId)
if let contactRequest = self.contactsService.contactRequest(withRingId: contactRingId) {
let discardCompleted = self.contactsService.discard(contactRequest: contactRequest,
withAccountId: accountId)
blockComplete = Observable<Void>.zip(discardCompleted, removeCompleted) { _, _ in
return
}
} else {
blockComplete = removeCompleted
}
blockComplete.asObservable()
.subscribe(onCompleted: { [weak self] in
if let conversation = self?.conversation.value {
self?.conversationsService.deleteConversation(conversation: conversation)
}
}).disposed(by: self.disposeBag)
}
func ban(withItem item: ContactRequestItem) -> Observable<Void> {
let accountId = item.contactRequest.accountId
let discardCompleted = self.contactsService.discard(contactRequest: item.contactRequest,
withAccountId: accountId)
let removeCompleted = self.contactsService.removeContact(withRingId: item.contactRequest.ringId,
ban: true,
withAccountId: accountId)
return Observable<Void>.zip(discardCompleted, removeCompleted) { _, _ in
return
}
}
}
......@@ -264,18 +264,6 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased
self.searchResultsTableView.isHidden = !isSearching
}).disposed(by: disposeBag)
//Show the Messages screens and pass the viewModel for Conversations
self.conversationsTableView.rx.modelSelected(ConversationViewModel.self).subscribe(onNext: { [unowned self] item in
self.cancelSearch()
self.viewModel.showConversation(withConversationViewModel: item)
}).disposed(by: disposeBag)
//Show the Messages screens and pass the viewModel for Search Results
self.searchResultsTableView.rx.modelSelected(ConversationViewModel.self).subscribe(onNext: { [unowned self] item in
self.cancelSearch()
self.viewModel.showConversation(withConversationViewModel: item)
}).disposed(by: disposeBag)
//Deselect the rows
self.conversationsTableView.rx.itemSelected.subscribe(onNext: { [unowned self] indexPath in
self.conversationsTableView.deselectRow(at: indexPath, animated: true)
......@@ -292,13 +280,7 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased
.disposed(by: disposeBag)
self.searchResultsTableView.rx.setDelegate(self).disposed(by: disposeBag)
//Swipe to delete action
self.conversationsTableView.rx.itemDeleted.subscribe(onNext: { [unowned self] indexPath in
if let convToDelete: ConversationViewModel = try? self.conversationsTableView.rx.model(at: indexPath) {
self.viewModel.delete(conversationViewModel: convToDelete)
}
}).disposed(by: disposeBag)
self.conversationsTableView.rx.setDelegate(self).disposed(by: disposeBag)
}
func setupSearchBar() {
......@@ -343,16 +325,67 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased
self.searchResultsTableView.isHidden = true
}
private func showDeleteConversationConfirmation(atIndex: IndexPath) {
let alert = UIAlertController(title: L10n.Alerts.confirmDeleteConversationTitle, message: L10n.Alerts.confirmDeleteConversation, preferredStyle: .alert)
let deleteAction = UIAlertAction(title: L10n.Actions.deleteAction, style: .destructive) { (_: UIAlertAction!) -> Void in
if let convToDelete: ConversationViewModel = try? self.conversationsTableView.rx.model(at: atIndex) {
self.viewModel.delete(conversationViewModel: convToDelete)
}
}
let cancelAction = UIAlertAction(title: L10n.Actions.cancelAction, style: .default) { (_: UIAlertAction!) -> Void in }
alert.addAction(deleteAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
private func showBlockContactConfirmation(atIndex: IndexPath) {
let alert = UIAlertController(title: L10n.Alerts.confirmBlockContactTitle, message: L10n.Alerts.confirmBlockContact, preferredStyle: .alert)
let blockAction = UIAlertAction(title: L10n.Actions.blockAction, style: .destructive) { (_: UIAlertAction!) -> Void in
if let conversation: ConversationViewModel = try? self.conversationsTableView.rx.model(at: atIndex) {
self.viewModel.blockConversationsContact(conversationViewModel: conversation)
}
}
let cancelAction = UIAlertAction(title: L10n.Actions.cancelAction, style: .default) { (_: UIAlertAction!) -> Void in }
alert.addAction(blockAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
}
extension SmartlistViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
if tableView == self.conversationsTableView {
return 0
}
return SmartlistConstants.firstSectionHeightForHeader
} else {
return SmartlistConstants.defaultSectionHeightForHeader
}
}
func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? {
let block = UITableViewRowAction(style: .normal, title: "Block") { _, index in
self.showBlockContactConfirmation(atIndex: index)
}
block.backgroundColor = .orange
let delete = UITableViewRowAction(style: .normal, title: "Delete") { _, index in
self.showDeleteConversationConfirmation(atIndex: index)
}
delete.backgroundColor = .red
return [delete, block]
}
private func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.cancelSearch()
if let convToShow: ConversationViewModel = try? tableView.rx.model(at: indexPath) {
self.viewModel.showConversation(withConversationViewModel: convToShow)
}
}
}
......@@ -225,6 +225,23 @@ class SmartlistViewModel: Stateable, ViewModel {
}
}
func blockConversationsContact(conversationViewModel: ConversationViewModel) {
if let index = self.conversationViewModels.index(where: ({ cvm in
cvm.conversation.value == conversationViewModel.conversation.value
})) {
let contactRingId = conversationViewModel.conversation.value.recipientRingId
let accountId = conversationViewModel.conversation.value.accountId
let removeCompleted = self.contactsService.removeContact(withRingId: contactRingId,
ban: true,
withAccountId: accountId)
removeCompleted.asObservable()
.subscribe(onCompleted: { [weak self] in
self?.conversationsService.deleteConversation(conversation: conversationViewModel.conversation.value)
self?.conversationViewModels.remove(at: index)
}).disposed(by: self.disposeBag)
}
}
func showConversation (withConversationViewModel conversationViewModel: ConversationViewModel) {
self.stateSubject.onNext(ConversationsState.conversationDetail(conversationViewModel: conversationViewModel))
}
......
......@@ -24,6 +24,7 @@ class ContactModel: Equatable {
var userName: String?
var confirmed: Bool = false
var added: Date = Date()
var banned: Bool = false
init(withRingId ringId: String) {
self.ringId = ringId
......@@ -43,6 +44,11 @@ class ContactModel: Equatable {
let addedDate = Date(timeIntervalSince1970: Double(added)!)
self.added = addedDate
}
if let banned = dictionary["banned"] {
if let banned = banned.toBool() {
self.banned = banned
}
}
}
public static func == (lhs: ContactModel, rhs: ContactModel) -> Bool {
......
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_block.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_block_2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_block_3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
......@@ -83,6 +83,15 @@
"alerts.accountLinkedTitle" = "Linking account";
"alerts.dbFailedTitle" = "An error happned when launching Ring";
"alerts.dbFailedMessage" = "Please close application and try to open it again";
"alerts.confirmBlockContact" = "Are you sure you want to block this contact? The conversation history with this contact will also be deleted permanently.";
"alerts.confirmBlockContactTitle" = "Block Contact";
"alerts.confirmDeleteConversation" = "Are you sure you want to delete this conversation permanently?";
"alerts.confirmDeleteConversationTitle" = "Delete Conversation";
//Actions
"actions.blockAction" = "Block";
"actions.deleteAction" = "Delete";
"actions.cancelAction" = "Cancel";
//Account Page
"accountPage.devicesListHeader" = "Devices";
......
......@@ -78,6 +78,7 @@ class ContactsService {
for contact in contacts {
if self.contacts.value.index(of: contact) == nil {
self.contacts.value.append(contact)
self.log.debug("contact: \(String(describing: contact.userName))")
}
}
}
......@@ -118,17 +119,17 @@ class ContactsService {
}
}
func discard(contactRequest: ContactRequestModel, withAccount account: AccountModel) -> Observable<Void> {
func discard(contactRequest: ContactRequestModel, withAccountId accountId: String) -> Observable<Void> {
return Observable.create { [unowned self] observable in
let success = self.contactsAdapter.discardTrustRequest(fromContact: contactRequest.ringId,
withAccountId: account.id)
withAccountId: accountId)
//Update the Contact request list
self.removeContactRequest(withRingId: contactRequest.ringId)
if success {
var event = ServiceEvent(withEventType: .contactRequestDiscarded)
event.addEventInput(.accountId, value: account.id)
event.addEventInput(.accountId, value: accountId)
event.addEventInput(.uri, value: contactRequest.ringId)
self.responseStream.onNext(event)
observable.on(.completed)
......@@ -169,13 +170,9 @@ class ContactsService {
}
}
func removeContact(contact: ContactModel, ban: Bool, withAccount account: AccountModel) -> Observable<Void> {
return removeContact(withRingId: contact.ringId, ban: ban, withAccount: account)
}
func removeContact(withRingId ringId: String, ban: Bool, withAccount account: AccountModel) -> Observable<Void> {
func removeContact(withRingId ringId: String, ban: Bool, withAccountId accountId: String) -> Observable<Void> {
return Observable.create { [unowned self] observable in
self.contactsAdapter.removeContact(withURI: ringId, accountId: account.id, ban: ban)
self.contactsAdapter.removeContact(withURI: ringId, accountId: accountId, ban: ban)
self.removeContactRequest(withRingId: ringId)
observable.on(.completed)
return Disposables.create { }
......@@ -191,16 +188,6 @@ class ContactsService {
}
self.contactRequests.value.remove(at: index)
}
fileprivate func removeContact(withRingId ringId: String) {
guard let contactToRemove = self.contacts.value.filter({ $0.ringId == ringId}).first else {
return
}
guard let index = self.contacts.value.index(where: { $0 === contactToRemove }) else {
return
}
self.contacts.value.remove(at: index)
}
}
extension ContactsService: ContactsAdapterDelegate {
......@@ -278,7 +265,11 @@ extension ContactsService: ContactsAdapterDelegate {
}
func contactRemoved(contact uri: String, withAccountId accountId: String, banned: Bool) {
self.removeContact(withRingId: uri)
guard let contactToRemove = self.contacts.value.filter({ $0.ringId == uri}).first else {
return
}
contactToRemove.banned = banned
self.contactStatus.onNext(contactToRemove)
log.debug("Contact removed :\(uri)")
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment