Browse Source

Merge pull request #782 from deltachat/mute_chats

Mute chats
bjoern 5 years ago
parent
commit
0725cfd1df

+ 8 - 0
DcCore/DcCore/DC/Wrapper.swift

@@ -294,6 +294,10 @@ public class DcContext {
         dc_configure(contextPointer)
     }
 
+    public func setChatMuteDuration(chatId: Int, duration: Int) {
+        dc_set_chat_mute_duration(self.contextPointer, UInt32(chatId), Int64(duration))
+    }
+
     public func getConfig(_ key: String) -> String? {
         guard let cString = dc_get_config(self.contextPointer, key) else { return nil }
         let value = String(cString: cString)
@@ -720,6 +724,10 @@ public class DcChat {
         return dc_chat_is_verified(chatPointer) > 0
     }
 
+    public var isMuted: Bool {
+        return dc_chat_is_muted(chatPointer) != 0
+    }
+
     public var contactIds: [Int] {
         return DcUtils.copyAndFreeArray(inputArray: dc_get_chat_contacts(DcContext.shared.contextPointer, UInt32(id)))
     }

+ 2 - 1
DcCore/DcCore/DC/events.swift

@@ -119,7 +119,8 @@ public func handleEvent(event: DcEvent) {
                     object: nil,
                     userInfo: userInfo)
 
-            if !UserDefaults.standard.bool(forKey: "notifications_disabled") {
+            let chat = DcContext.shared.getChat(chatId: Int(data1))
+            if !UserDefaults.standard.bool(forKey: "notifications_disabled") && !chat.isMuted {
                 let content = UNMutableNotificationContent()
                 let msg = DcMsg(id: Int(data2))
                 content.title = msg.fromContact.displayName

+ 23 - 0
deltachat-ios/Assets.xcassets/volume_off.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "filename" : "baseline_volume_off_white_24pt_1x.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "baseline_volume_off_white_24pt_2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "baseline_volume_off_white_24pt_3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
deltachat-ios/Assets.xcassets/volume_off.imageset/baseline_volume_off_white_24pt_1x.png


BIN
deltachat-ios/Assets.xcassets/volume_off.imageset/baseline_volume_off_white_24pt_2x.png


BIN
deltachat-ios/Assets.xcassets/volume_off.imageset/baseline_volume_off_white_24pt_3x.png


+ 17 - 3
deltachat-ios/Controller/ChatViewController.swift

@@ -61,6 +61,17 @@ class ChatViewController: MessagesViewController {
         return UIBarButtonItem(customView: indicator)
     }()
 
+
+    private lazy var muteItem: UIBarButtonItem = {
+        let imageView = UIImageView()
+        imageView.tintColor = DcColors.defaultTextColor
+        imageView.image =  #imageLiteral(resourceName: "volume_off").withRenderingMode(.alwaysTemplate)
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
+        imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true
+        return UIBarButtonItem(customView: imageView)
+    }()
+
     private lazy var badgeItem: UIBarButtonItem = {
         let badge: InitialsBadge
         let chat = dcContext.getChat(chatId: chatId)
@@ -298,11 +309,14 @@ class ChatViewController: MessagesViewController {
         titleView.updateTitleView(title: chat.name, subtitle: subtitle)
         navigationItem.titleView = titleView
 
+        var rightBarButtonItems = [badgeItem]
         if chat.isSendingLocations {
-            navigationItem.rightBarButtonItems = [badgeItem, locationStreamingItem]
-        } else {
-            navigationItem.rightBarButtonItems = [badgeItem]
+            rightBarButtonItems.append(locationStreamingItem)
+        }
+        if chat.isMuted {
+            rightBarButtonItems.append(muteItem)
         }
+        navigationItem.rightBarButtonItems = rightBarButtonItems
     }
 
     @objc

+ 47 - 13
deltachat-ios/Controller/ContactDetailViewController.swift

@@ -33,6 +33,14 @@ class ContactDetailViewController: UITableViewController {
         return cell
     }()
 
+    private lazy var muteChatCell: ActionCell = {
+        let cell = ActionCell()
+        cell.actionTitle = viewModel.chatIsMuted ? String.localized("menu_unmute") :  String.localized("menu_mute")
+        cell.actionColor = SystemColor.blue.uiColor
+        cell.selectionStyle = .none
+        return cell
+    }()
+
     private lazy var archiveChatCell: ActionCell = {
         let cell = ActionCell()
         cell.actionTitle = viewModel.chatIsArchived ? String.localized("menu_unarchive_chat") :  String.localized("menu_archive_chat")
@@ -53,7 +61,7 @@ class ContactDetailViewController: UITableViewController {
         let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
         cell.textLabel?.text = String.localized("gallery")
         cell.accessoryType = .disclosureIndicator
-        if viewModel.chatId == nil {
+        if viewModel.chatId == 0 {
             cell.isUserInteractionEnabled = false
             cell.textLabel?.isEnabled = false
         }
@@ -64,7 +72,7 @@ class ContactDetailViewController: UITableViewController {
         let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
         cell.textLabel?.text = String.localized("documents")
         cell.accessoryType = .disclosureIndicator
-        if viewModel.chatId == nil {
+        if viewModel.chatId == 0 {
             cell.isUserInteractionEnabled = false
             cell.textLabel?.isEnabled = false
         }
@@ -128,6 +136,8 @@ class ContactDetailViewController: UITableViewController {
             }
         case .chatActions:
             switch viewModel.chatActionFor(row: row) {
+            case .muteChat:
+                return muteChatCell
             case .archiveChat:
                 return archiveChatCell
             case .blockContact:
@@ -195,6 +205,13 @@ class ContactDetailViewController: UITableViewController {
     private func handleCellAction(for index: Int) {
         let action = viewModel.chatActionFor(row: index)
         switch action {
+        case .muteChat:
+            if viewModel.chatIsMuted {
+                self.viewModel.context.setChatMuteDuration(chatId: self.viewModel.chatId, duration: 0)
+                muteChatCell.actionTitle = String.localized("menu_mute")
+            } else {
+                showMuteAlert()
+            }
         case .archiveChat:
             toggleArchiveChat()
         case .blockContact:
@@ -248,14 +265,26 @@ class ContactDetailViewController: UITableViewController {
         self.present(alert, animated: true, completion: nil)
     }
 
-    private func showNotificationSetup() {
-        let notificationSetupAlert = UIAlertController(
-            title: "Notifications Setup is not implemented yet",
-            message: "But you get an idea where this is going",
-            preferredStyle: .safeActionSheet)
+    private func showMuteAlert() {
+        let alert = UIAlertController(title: String.localized("mute"), message: nil, preferredStyle: .safeActionSheet)
+        let forever = -1
+        addDurationSelectionAction(to: alert, key: "mute_for_one_hour", duration: Time.oneHour)
+        addDurationSelectionAction(to: alert, key: "mute_for_one_hour", duration: Time.twoHours)
+        addDurationSelectionAction(to: alert, key: "mute_for_one_day", duration: Time.oneDay)
+        addDurationSelectionAction(to: alert, key: "mute_for_seven_days", duration: Time.oneWeek)
+        addDurationSelectionAction(to: alert, key: "mute_forever", duration: forever)
+
         let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
-        notificationSetupAlert.addAction(cancelAction)
-        present(notificationSetupAlert, animated: true, completion: nil)
+        alert.addAction(cancelAction)
+        present(alert, animated: true, completion: nil)
+    }
+
+    private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
+        let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
+            self.viewModel.context.setChatMuteDuration(chatId: self.viewModel.chatId, duration: duration)
+            self.muteChatCell.actionTitle = String.localized("menu_unmute")
+        })
+        alert.addAction(action)
     }
 
     private func toggleBlockContact() {
@@ -305,8 +334,13 @@ class ContactDetailViewController: UITableViewController {
     }
 
     private func presentPreview(for messageType: Int32, messageType2: Int32, messageType3: Int32) {
-        guard let chatId = viewModel.chatId else { return }
-        let messageIds = viewModel.context.getChatMedia(chatId: chatId, messageType: messageType, messageType2: messageType2, messageType3: messageType3)
+        if viewModel.chatId == 0 {
+            return
+        }
+        let messageIds = viewModel.context.getChatMedia(chatId: viewModel.chatId,
+                                                        messageType: messageType,
+                                                        messageType2: messageType2,
+                                                        messageType3: messageType3)
         var mediaUrls: [URL] = []
         for messageId in messageIds {
             let message = DcMsg.init(id: messageId)
@@ -319,10 +353,10 @@ class ContactDetailViewController: UITableViewController {
     }
 
     private func deleteChat() {
-        guard let chatId = viewModel.chatId else {
+        if viewModel.chatId == 0 {
             return
         }
-        viewModel.context.deleteChat(chatId: chatId)
+        viewModel.context.deleteChat(chatId: viewModel.chatId)
 
         // just pop to viewControllers - we've in chatlist or archive then
         // (no not use `navigationController?` here: popping self will make the reference becoming nil)

+ 51 - 10
deltachat-ios/Controller/GroupChatDetailViewController.swift

@@ -14,9 +14,10 @@ class GroupChatDetailViewController: UIViewController {
     private let membersRowAddMembers = 0
     private let membersRowQrInvite = 1
     private let memberManagementRows = 2
-    private let chatActionsRowArchiveChat = 0
-    private let chatActionsRowLeaveGroup = 1
-    private let chatActionsRowDeleteChat = 2
+    private let chatActionsRowMuteChat = 0
+    private let chatActionsRowArchiveChat = 1
+    private let chatActionsRowLeaveGroup = 2
+    private let chatActionsRowDeleteChat = 3
 
     private let dcContext: DcContext
 
@@ -31,7 +32,9 @@ class GroupChatDetailViewController: UIViewController {
     }
 
     private var chatId: Int
-    fileprivate var chat: DcChat
+    private var chat: DcChat {
+        return dcContext.getChat(chatId: chatId)
+    }
 
     // stores contactIds
     private var groupMemberIds: [Int] = []
@@ -68,6 +71,15 @@ class GroupChatDetailViewController: UIViewController {
         return header
     }()
 
+    private lazy var muteChatCell: ActionCell = {
+        let cell = ActionCell()
+        cell.actionTitle = self.chat.isMuted ? String.localized("menu_unmute") :  String.localized("menu_mute")
+        cell.actionColor = SystemColor.blue.uiColor
+        cell.selectionStyle = .none
+        return cell
+    }()
+
+
     private lazy var archiveChatCell: ActionCell = {
         let cell = ActionCell()
         cell.actionTitle = chat.isArchived ? String.localized("menu_unarchive_chat") :  String.localized("menu_archive_chat")
@@ -109,7 +121,6 @@ class GroupChatDetailViewController: UIViewController {
     init(chatId: Int, dcContext: DcContext) {
         self.dcContext = dcContext
         self.chatId = chatId
-        chat = dcContext.getChat(chatId: chatId)
         super.init(nibName: nil, bundle: nil)
         setupSubviews()
     }
@@ -139,7 +150,6 @@ class GroupChatDetailViewController: UIViewController {
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         //update chat object, maybe chat name was edited
-        chat = dcContext.getChat(chatId: chat.id)
         updateGroupMembers()
         tableView.reloadData() // to display updates
         editBarButtonItem.isEnabled = currentUser != nil
@@ -178,7 +188,6 @@ class GroupChatDetailViewController: UIViewController {
         } else {
             self.navigationController?.popToRootViewController(animated: false)
         }
-        self.chat = dcContext.getChat(chatId: chat.id)
      }
 
     private func getGroupMemberIdFor(_ row: Int) -> Int {
@@ -263,7 +272,7 @@ extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSou
         case .members:
             return groupMemberIds.count + memberManagementRows
         case .chatActions:
-            return 3
+            return 4
         }
     }
 
@@ -322,7 +331,9 @@ extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSou
             contactCell.updateCell(cellViewModel: cellViewModel)
             return contactCell
         case .chatActions:
-            if row == chatActionsRowArchiveChat {
+            if row == chatActionsRowMuteChat {
+                return muteChatCell
+            } else if row == chatActionsRowArchiveChat {
                 return archiveChatCell
             } else if row == chatActionsRowLeaveGroup {
                 return leaveGroupCell
@@ -355,7 +366,14 @@ extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSou
                 showContactDetail(of: member.id)
             }
         case .chatActions:
-            if row == chatActionsRowArchiveChat {
+            if row == chatActionsRowMuteChat {
+                if chat.isMuted {
+                    dcContext.setChatMuteDuration(chatId: chatId, duration: 0)
+                    muteChatCell.actionTitle = String.localized("menu_mute")
+                } else {
+                    showMuteAlert()
+                }
+            } else if row == chatActionsRowArchiveChat {
                 toggleArchiveChat()
             } else if row == chatActionsRowLeaveGroup {
                 showLeaveGroupConfirmationAlert()
@@ -433,6 +451,29 @@ extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSou
 
 // MARK: - alerts
 extension GroupChatDetailViewController {
+
+    private func showMuteAlert() {
+        let alert = UIAlertController(title: String.localized("mute"), message: nil, preferredStyle: .safeActionSheet)
+        let forever = -1
+        addDurationSelectionAction(to: alert, key: "mute_for_one_hour", duration: Time.oneHour)
+        addDurationSelectionAction(to: alert, key: "mute_for_one_hour", duration: Time.twoHours)
+        addDurationSelectionAction(to: alert, key: "mute_for_one_day", duration: Time.oneDay)
+        addDurationSelectionAction(to: alert, key: "mute_for_seven_days", duration: Time.oneWeek)
+        addDurationSelectionAction(to: alert, key: "mute_forever", duration: forever)
+
+        let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
+        alert.addAction(cancelAction)
+        present(alert, animated: true, completion: nil)
+    }
+
+    private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
+        let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
+            self.dcContext.setChatMuteDuration(chatId: self.chatId, duration: duration)
+            self.muteChatCell.actionTitle = String.localized("menu_unmute")
+        })
+        alert.addAction(action)
+    }
+
     private func showDeleteChatConfirmationAlert() {
         let alert = UIAlertController(
             title: nil,

+ 2 - 0
deltachat-ios/Helper/Constants.swift

@@ -28,4 +28,6 @@ struct Time {
     static let oneHour = 60 * 60
     static let twoHours = 2 * 60 * 60
     static let sixHours = 6 * 60 * 60
+    static let oneDay = 24 * 60 * 60
+    static let oneWeek = 7 * 24 * 60 * 60
 }

+ 28 - 8
deltachat-ios/View/ContactCell.swift

@@ -15,7 +15,7 @@ class ContactCell: UITableViewCell {
     private let imgSize: CGFloat = 20
 
     lazy var toplineStackView: UIStackView = {
-        let stackView = UIStackView(arrangedSubviews: [titleLabel, pinnedIndicator, timeLabel, locationStreamingIndicator])
+        let stackView = UIStackView(arrangedSubviews: [titleLabel, mutedIndicator, pinnedIndicator, timeLabel, locationStreamingIndicator])
         stackView.axis = .horizontal
         stackView.alignment = .firstBaseline
         stackView.spacing = 4
@@ -56,12 +56,24 @@ class ContactCell: UITableViewCell {
         return view
     }()
 
+    private let mutedIndicator: UIImageView = {
+        let view = UIImageView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.heightAnchor.constraint(equalToConstant: 16).isActive = true
+        view.widthAnchor.constraint(equalToConstant: 16).isActive = true
+        view.tintColor = DcColors.middleGray
+        view.image = #imageLiteral(resourceName: "volume_off").withRenderingMode(.alwaysTemplate)
+        view.isHidden = true
+        return view
+    }()
+
     private let timeLabel: UILabel = {
         let label = UILabel()
         label.font = UIFont.systemFont(ofSize: 14)
         label.textColor = DcColors.middleGray
         label.textAlignment = .right
-        label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 2), for: NSLayoutConstraint.Axis.horizontal)
+        label.setContentHuggingPriority(.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
+        label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 10), for: NSLayoutConstraint.Axis.horizontal)
         return label
     }()
 
@@ -180,17 +192,17 @@ class ContactCell: UITableViewCell {
         avatar.setName(name)
     }
 
-    func setStatusIndicators(unreadCount: Int, status: Int, visibility: Int32, isLocationStreaming: Bool) {
+    func setStatusIndicators(unreadCount: Int, status: Int, visibility: Int32, isLocationStreaming: Bool, isMuted: Bool) {
         if visibility==DC_CHAT_VISIBILITY_ARCHIVED {
             pinnedIndicator.isHidden = true
             unreadMessageCounter.isHidden = true
             deliveryStatusIndicator.isHidden = true
             archivedIndicator.isHidden = false
         } else if unreadCount > 0 {
-            unreadMessageCounter.setCount(unreadCount)
-
             pinnedIndicator.isHidden = !(visibility==DC_CHAT_VISIBILITY_PINNED)
+            unreadMessageCounter.setCount(unreadCount)
             unreadMessageCounter.isHidden = false
+            unreadMessageCounter.backgroundColor = isMuted ? .gray : .red
             deliveryStatusIndicator.isHidden = true
             archivedIndicator.isHidden = true
         } else {
@@ -213,6 +225,7 @@ class ContactCell: UITableViewCell {
             archivedIndicator.isHidden = true
         }
 
+        mutedIndicator.isHidden = !isMuted
         locationStreamingIndicator.isHidden = !isLocationStreaming
     }
 
@@ -276,8 +289,11 @@ class ContactCell: UITableViewCell {
             }
             setVerified(isVerified: chat.isVerified)
             setTimeLabel(chatData.summary.timestamp)
-            setStatusIndicators(unreadCount: chatData.unreadMessages, status: chatData.summary.state,
-                                visibility: chat.visibility, isLocationStreaming: chat.isSendingLocations)
+            setStatusIndicators(unreadCount: chatData.unreadMessages,
+                                status: chatData.summary.state,
+                                visibility: chat.visibility,
+                                isLocationStreaming: chat.isSendingLocations,
+                                isMuted: chat.isMuted)
 
         case .contact(let contactData):
             let contact = DcContact(id: contactData.contactId)
@@ -289,7 +305,11 @@ class ContactCell: UITableViewCell {
                 avatar.setColor(contact.color)
             }
             setVerified(isVerified: contact.isVerified)
-            setStatusIndicators(unreadCount: 0, status: 0, visibility: 0, isLocationStreaming: false)
+            setStatusIndicators(unreadCount: 0,
+                                status: 0,
+                                visibility: 0,
+                                isLocationStreaming: false,
+                                isMuted: false)
         }
     }
 }

+ 12 - 10
deltachat-ios/ViewModel/ContactDetailViewModel.swift

@@ -13,6 +13,7 @@ class ContactDetailViewModel {
     }
 
     enum ChatAction {
+        case muteChat
         case archiveChat
         case blockContact
         case deleteChat
@@ -29,16 +30,16 @@ class ContactDetailViewModel {
         return DcContact(id: contactId)
     }
 
-    let chatId: Int?
+    let chatId: Int
     private let sharedChats: DcChatlist
     private var sections: [ProfileSections] = []
-    private var chatActions: [ChatAction] = [] // chatDetail: archive, block, delete - else: block
+    private var chatActions: [ChatAction] = []
     private var attachmentActions: [AttachmentAction] = [.gallery, .documents]
 
     init(dcContext: DcContext, contactId: Int) {
         self.context = dcContext
         self.contactId = contactId
-        self.chatId = dcContext.getChatIdByContactIdOld(contactId)
+        self.chatId = dcContext.getChatIdByContactId(contactId: contactId)
         self.sharedChats = context.getChatlist(flags: 0, queryString: nil, queryId: contactId)
 
         sections.append(.attachments)
@@ -48,8 +49,8 @@ class ContactDetailViewModel {
         }
         sections.append(.chatActions)
 
-        if chatId != nil {
-            chatActions = [.archiveChat, .blockContact, .deleteChat]
+        if chatId != 0 {
+            chatActions = [.muteChat, .archiveChat, .blockContact, .deleteChat]
         } else {
             chatActions = [.blockContact]
         }
@@ -68,10 +69,11 @@ class ContactDetailViewModel {
     }
 
     var chatIsArchived: Bool {
-        guard let chatId = chatId else {
-            return false
-        }
-        return context.getChat(chatId: chatId).isArchived
+        return chatId != 0 && context.getChat(chatId: chatId).isArchived
+    }
+
+    var chatIsMuted: Bool {
+        return chatId != 0 && context.getChat(chatId: chatId).isMuted
     }
 
     var numberOfSections: Int {
@@ -111,7 +113,7 @@ class ContactDetailViewModel {
 
     // returns true if chat is archived after action
     func toggleArchiveChat() -> Bool {
-        guard let chatId = chatId else {
+        if chatId == 0 {
             safe_fatalError("there is no chatId - you are probably are calling this from ContactDetail - this should be only called from ChatDetail")
             return false
         }