Forráskód Böngészése

Merge pull request #527 from deltachat/archiveDeletaInGroupChats

 Archive + Delete in GroupChatDetail and Archive in SingleChatDetail
bjoern 5 éve
szülő
commit
a08f321200

+ 32 - 28
deltachat-ios/Controller/ChatListController.swift

@@ -5,7 +5,7 @@ class ChatListController: UIViewController {
 
     private var dcContext: DcContext
     private var chatList: DcChatlist?
-    private var showArchive: Bool
+    private let showArchive: Bool
 
     private lazy var chatTable: UITableView = {
         let chatTable = UITableView()
@@ -42,31 +42,28 @@ class ChatListController: UIViewController {
         super.viewWillAppear(animated)
         getChatList()
         updateTitle()
-    }
 
-    override func viewDidAppear(_ animated: Bool) {
-        super.viewDidAppear(animated)
         let nc = NotificationCenter.default
-        msgChangedObserver = nc.addObserver(forName: dcNotificationChanged,
-                                            object: nil, queue: nil) { _ in
-            self.getChatList()
-        }
-        incomingMsgObserver = nc.addObserver(forName: dcNotificationIncoming,
-                                             object: nil, queue: nil) { _ in
-            self.getChatList()
-        }
-
-        viewChatObserver = nc.addObserver(forName: dcNotificationViewChat, object: nil, queue: nil) { notification in
-            if let chatId = notification.userInfo?["chat_id"] as? Int {
-                self.coordinator?.showChat(chatId: chatId)
-            }
-        }
-
-        deleteChatObserver = nc.addObserver(forName: dcNotificationChatDeletedInChatDetail, object: nil, queue: nil) { notification in
-            if let chatId = notification.userInfo?["chat_id"] as? Int {
-                self.deleteChat(chatId: chatId, animated: true)
-            }
-        }
+          msgChangedObserver = nc.addObserver(forName: dcNotificationChanged,
+                                              object: nil, queue: nil) { _ in
+              self.getChatList()
+          }
+          incomingMsgObserver = nc.addObserver(forName: dcNotificationIncoming,
+                                               object: nil, queue: nil) { _ in
+              self.getChatList()
+          }
+
+          viewChatObserver = nc.addObserver(forName: dcNotificationViewChat, object: nil, queue: nil) { notification in
+              if let chatId = notification.userInfo?["chat_id"] as? Int {
+                  self.coordinator?.showChat(chatId: chatId)
+              }
+          }
+
+          deleteChatObserver = nc.addObserver(forName: dcNotificationChatDeletedInChatDetail, object: nil, queue: nil) { notification in
+              if let chatId = notification.userInfo?["chat_id"] as? Int {
+                  self.deleteChat(chatId: chatId, animated: true)
+              }
+          }
     }
 
     override func viewDidDisappear(_ animated: Bool) {
@@ -82,6 +79,10 @@ class ChatListController: UIViewController {
         if let viewChatObserver = self.viewChatObserver {
             nc.removeObserver(viewChatObserver)
         }
+
+        if let deleteChatObserver = self.deleteChatObserver {
+            nc.removeObserver(deleteChatObserver)
+        }
     }
 
     override func viewDidLoad() {
@@ -314,8 +315,9 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
     }
 
     private func deleteChat(chatId: Int, animated: Bool) {
-        self.dcContext.deleteChat(chatId: chatId)
+
         if !animated {
+            dcContext.deleteChat(chatId: chatId)
             self.getChatList()
             return
         }
@@ -325,9 +327,9 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
         }
 
         // find index of chatId
-        let index = Array(0..<chatList.length).filter { chatList.getChatId(index: $0) == chatId }.first
+        let indexToDelete = Array(0..<chatList.length).filter { chatList.getChatId(index: $0) == chatId }.first
 
-        guard let row = index else {
+        guard let row = indexToDelete else {
             return
         }
 
@@ -335,7 +337,9 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
         if showArchive {
             gclFlags |= DC_GCL_ARCHIVED_ONLY
         }
+
+        dcContext.deleteChat(chatId: chatId)
         self.chatList = dcContext.getChatlist(flags: gclFlags, queryString: nil, queryId: 0)
-        chatTable.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)    
+        chatTable.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)
     }
 }

+ 66 - 50
deltachat-ios/Controller/ContactDetailViewController.swift

@@ -96,12 +96,12 @@ class ContactDetailViewController: UITableViewController {
         let cellType = viewModel.typeFor(section: indexPath.section)
         switch cellType {
         case .chatActions:
-            if row == 0 {
+            switch viewModel.actionFor(row: row) {
+            case .archiveChat:
                 return archiveChatCell
-            } else if row == 1 {
+            case .blockChat:
                 return blockContactCell
-            } else {
-                safe_assert(row == 2)
+            case .deleteChat:
                 return deleteChatCell
             }
         case .startChat:
@@ -143,52 +143,26 @@ class ContactDetailViewController: UITableViewController {
         return viewModel.titleFor(section: section)
     }
 
-    // MARK: -actions
+    // MARK: - actions
 
     private func handleCellAction(for index: Int) {
-        if index == 0 {
-            coordinator?.archiveChat()
-        } else if index == 1 {
+        let action = viewModel.actionFor(row: index)
+        switch action {
+        case .archiveChat:
+            toggleArchiveChat()
+        case .blockChat:
             toggleBlockContact()
-        } else {
-            safe_assert(index == 2)
+        case .deleteChat:
             showDeleteChatConfirmationAlert()
         }
-
-    }
-    private func askToChatWith(contactId: Int) {
-        let dcContact = DcContact(id: contactId)
-        let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), dcContact.nameNAddr),
-                                      message: nil,
-                                      preferredStyle: .safeActionSheet)
-        alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
-            self.dismiss(animated: true, completion: nil)
-            let chatId = Int(dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId)))
-            self.coordinator?.showChat(chatId: chatId)
-        }))
-        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
-            self.dismiss(animated: true, completion: nil)
-        }))
-        present(alert, animated: true, completion: nil)
     }
 
-    private func toggleBlockContact() {
-        if viewModel.contact.isBlocked {
-            let alert = UIAlertController(title: String.localized("ask_unblock_contact"), message: nil, preferredStyle: .safeActionSheet)
-            alert.addAction(UIAlertAction(title: String.localized("menu_unblock_contact"), style: .default, handler: { _ in
-                self.viewModel.contact.unblock()
-                self.updateBlockContactCell()
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
-            present(alert, animated: true, completion: nil)
+    private func toggleArchiveChat() {
+        let archived = viewModel.toggleArchiveChat()
+        if archived {
+            self.navigationController?.popToRootViewController(animated: false)
         } else {
-            let alert = UIAlertController(title: String.localized("ask_block_contact"), message: nil, preferredStyle: .safeActionSheet)
-            alert.addAction(UIAlertAction(title: String.localized("menu_block_contact"), style: .destructive, handler: { _ in
-                self.viewModel.contact.block()
-                self.updateBlockContactCell()
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
-            present(alert, animated: true, completion: nil)
+            archiveChatCell.actionTitle = String.localized("menu_archive_chat")
         }
     }
 
@@ -197,19 +171,14 @@ class ContactDetailViewController: UITableViewController {
         blockContactCell.actionColor = viewModel.contact.isBlocked ? SystemColor.blue.uiColor : UIColor.red
     }
 
-    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)
-        let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
-        notificationSetupAlert.addAction(cancelAction)
-        present(notificationSetupAlert, animated: true, completion: nil)
-    }
 
     @objc private func editButtonPressed() {
         coordinator?.showEditContact(contactId: viewModel.contactId)
     }
+}
 
+// MARK: alerts
+extension ContactDetailViewController {
     private func showDeleteChatConfirmationAlert() {
         let alert = UIAlertController(
             title: nil,
@@ -223,4 +192,51 @@ 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)
+        let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
+        notificationSetupAlert.addAction(cancelAction)
+        present(notificationSetupAlert, animated: true, completion: nil)
+    }
+
+    private func toggleBlockContact() {
+        if viewModel.contact.isBlocked {
+            let alert = UIAlertController(title: String.localized("ask_unblock_contact"), message: nil, preferredStyle: .safeActionSheet)
+            alert.addAction(UIAlertAction(title: String.localized("menu_unblock_contact"), style: .default, handler: { _ in
+                self.viewModel.contact.unblock()
+                self.updateBlockContactCell()
+            }))
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+            present(alert, animated: true, completion: nil)
+        } else {
+            let alert = UIAlertController(title: String.localized("ask_block_contact"), message: nil, preferredStyle: .safeActionSheet)
+            alert.addAction(UIAlertAction(title: String.localized("menu_block_contact"), style: .destructive, handler: { _ in
+                self.viewModel.contact.block()
+                self.updateBlockContactCell()
+            }))
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+            present(alert, animated: true, completion: nil)
+        }
+    }
+
+    private func askToChatWith(contactId: Int) {
+        let dcContact = DcContact(id: contactId)
+        let alert = UIAlertController(title: String.localizedStringWithFormat(
+            String.localized("ask_start_chat_with"), dcContact.nameNAddr),
+                                      message: nil,
+                                      preferredStyle: .safeActionSheet)
+        alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
+            self.dismiss(animated: true, completion: nil)
+            let chatId = Int(dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId)))
+            self.coordinator?.showChat(chatId: chatId)
+        }))
+        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
+            self.dismiss(animated: true, completion: nil)
+        }))
+        present(alert, animated: true, completion: nil)
+    }
+
 }

+ 224 - 161
deltachat-ios/Controller/GroupChatDetailViewController.swift

@@ -2,35 +2,89 @@ import UIKit
 
 class GroupChatDetailViewController: UIViewController {
 
-    private let sectionMembers = 0
-    private let sectionMembersRowAddMember = 0
-    private let sectionMembersRowJoinQR = 1
-    private let sectionMembersStaticRowCount = 2 // followed by one row per member
+    enum ProfileSections {
+        case memberManagement // add member, qr invideCode
+        case members // contactCells
+        case chatActions // archive, leave, delete
+    }
 
-    private let sectionLeaveGroup = 1
-    private let sectionLeaveGroupRowCount = 1
+    private let context: DcContext
+    weak var coordinator: GroupChatDetailCoordinator?
 
-    private let sectionCount = 2
+    private let sections: [ProfileSections] = [.memberManagement, .members, .chatActions]
 
     private var currentUser: DcContact? {
-        return groupMembers.filter { $0.email == DcConfig.addr }.first
+        let myId = groupMemberIds.filter { DcContact(id: $0).email == DcConfig.addr }.first
+        guard let currentUserId = myId else {
+            return nil
+        }
+        return DcContact(id: currentUserId)
     }
 
-    weak var coordinator: GroupChatDetailCoordinator?
-
     fileprivate var chat: DcChat
 
-    var chatDetailTable: UITableView = {
+    // stores contactIds
+    private var groupMemberIds: [Int] = []
+
+    // MARK: -subviews
+
+    private lazy var editBarButtonItem: UIBarButtonItem = {
+        UIBarButtonItem(title: String.localized("global_menu_edit_desktop"), style: .plain, target: self, action: #selector(editButtonPressed))
+    }()
+
+    lazy var tableView: UITableView = {
         let table = UITableView(frame: .zero, style: .grouped)
         table.bounces = false
         table.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
         table.register(ActionCell.self, forCellReuseIdentifier: "actionCell")
         table.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
-
+        table.delegate = self
+        table.dataSource = self
+        table.tableHeaderView = groupHeader
         return table
     }()
 
-    init(chatId: Int) {
+    private lazy var groupHeader: ContactDetailHeader = {
+        let header = ContactDetailHeader()
+        header.updateDetails(
+            title: chat.name,
+            subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
+        )
+        if let img = chat.profileImage {
+            header.setImage(img)
+        } else {
+            header.setBackupImage(name: chat.name, color: chat.color)
+        }
+        header.setVerified(isVerified: chat.isVerified)
+        return header
+    }()
+
+    private lazy var archiveChatCell: ActionCell = {
+        let cell = ActionCell()
+        cell.actionTitle = chat.isArchived ? String.localized("menu_unarchive_chat") :  String.localized("menu_archive_chat")
+        cell.actionColor = SystemColor.blue.uiColor
+        cell.selectionStyle = .none
+        return cell
+    }()
+
+    private lazy var leaveGroupCell: ActionCell = {
+        let cell = ActionCell()
+        cell.actionTitle = String.localized("menu_leave_group")
+        cell.actionColor = UIColor.red
+        return cell
+    }()
+
+
+    private lazy var deleteChatCell: ActionCell = {
+        let cell = ActionCell()
+        cell.actionTitle = String.localized("menu_delete_chat")
+        cell.actionColor = UIColor.red
+        cell.selectionStyle = .none
+        return cell
+    }()
+
+    init(chatId: Int, context: DcContext) {
+        self.context = context
         chat = DcChat(id: chatId)
         super.init(nibName: nil, bundle: nil)
         setupSubviews()
@@ -41,209 +95,187 @@ class GroupChatDetailViewController: UIViewController {
     }
 
     private func setupSubviews() {
-        view.addSubview(chatDetailTable)
-        chatDetailTable.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(tableView)
+        tableView.translatesAutoresizingMaskIntoConstraints = false
 
-        chatDetailTable.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
-        chatDetailTable.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
-        chatDetailTable.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
-        chatDetailTable.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
+        tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
+        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
+        tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
+        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
     }
 
-    private lazy var editBarButtonItem: UIBarButtonItem = {
-        UIBarButtonItem(title: String.localized("global_menu_edit_desktop"), style: .plain, target: self, action: #selector(editButtonPressed))
-    }()
-
-    private var groupMembers: [DcContact] = []
-
+    // MARK: - lifecycle
     override func viewDidLoad() {
         super.viewDidLoad()
         title = String.localized("tab_group")
-        chatDetailTable.delegate = self
-        chatDetailTable.dataSource = self
         navigationItem.rightBarButtonItem = editBarButtonItem
+        groupHeader.frame = CGRect(0, 0, tableView.frame.width, ContactCell.cellHeight)
     }
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         updateGroupMembers()
-        chatDetailTable.reloadData() // to display updates
+        tableView.reloadData() // to display updates
         editBarButtonItem.isEnabled = currentUser != nil
+        updateHeader()
         //update chat object, maybe chat name was edited
         chat = DcChat(id: chat.id)
     }
 
+    // MARK: - update
     private func updateGroupMembers() {
-        let ids = chat.contactIds
-        groupMembers = ids.map { DcContact(id: $0) }
-        chatDetailTable.reloadData()
+        groupMemberIds = chat.contactIds
+        tableView.reloadData()
+    }
+
+    private func updateHeader() {
+        groupHeader.updateDetails(
+            title: chat.name,
+            subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
+        )
     }
 
+    // MARK: - actions
     @objc func editButtonPressed() {
         coordinator?.showGroupChatEdit(chat: chat)
     }
 
-    private func leaveGroup() {
-        if let userId = currentUser?.id {
-            let alert = UIAlertController(title: String.localized("ask_leave_group"), message: nil, preferredStyle: .safeActionSheet)
-            alert.addAction(UIAlertAction(title: String.localized("menu_leave_group"), style: .destructive, handler: { _ in
-                dc_remove_contact_from_chat(mailboxPointer, UInt32(self.chat.id), UInt32(userId))
-                self.editBarButtonItem.isEnabled = false
-                self.updateGroupMembers()
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
-            present(alert, animated: true, completion: nil)
+    private func toggleArchiveChat() {
+        let archivedBefore = chat.isArchived
+        context.archiveChat(chatId: chat.id, archive: !archivedBefore)
+        if archivedBefore {
+            archiveChatCell.actionTitle = String.localized("menu_archive_chat")
+        } else {
+            self.navigationController?.popToRootViewController(animated: false)
         }
-    }
+        self.chat = DcChat(id: chat.id)
+     }
 }
 
+// MARK: - UITableViewDelegate, UITableViewDataSource
 extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSource {
 
-    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
-        if section == sectionMembers {
-            return ContactDetailHeader.cellHeight
-        }
-        return 0
-    }
-    
-    func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? {
-        if section == sectionMembers {
-            let header = ContactDetailHeader()
-            header.updateDetails(title: chat.name,
-                                 subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count))
-            if let img = chat.profileImage {
-                header.setImage(img)
-            } else {
-                header.setBackupImage(name: chat.name, color: chat.color)
-            }
-            header.setVerified(isVerified: chat.isVerified)
-            return header
-        } else {
-            return nil
-        }
-    }
-
     func numberOfSections(in _: UITableView) -> Int {
-        if currentUser == nil {
-            return sectionCount-1 // leave out last section (leaveSection)
-        }
-        return sectionCount
+        return sections.count
     }
 
     func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
-        switch section {
-        case sectionMembers:
-            return sectionMembersStaticRowCount + groupMembers.count
-        case sectionLeaveGroup:
-            return sectionLeaveGroupRowCount
-        default:
-            return 0
+        let sectionType = sections[section]
+        switch sectionType {
+        case .memberManagement:
+            return 2
+        case .members:
+            return groupMemberIds.count
+        case .chatActions:
+            return 3
         }
     }
 
     func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
-        let section = indexPath.section
-        let row = indexPath.row
-        switch section {
-        case sectionMembers:
-            switch row {
-            case sectionMembersRowAddMember:
-                return Constants.defaultCellHeight
-            case sectionMembersRowJoinQR:
-                return Constants.defaultCellHeight
-            default:
-                return ContactCell.cellHeight
-            }
-        case sectionLeaveGroup:
+        let sectionType = sections[indexPath.section]
+        switch sectionType {
+        case .memberManagement:
+            return Constants.defaultCellHeight
+        case .members:
+            return ContactCell.cellHeight
+        case .chatActions:
             return Constants.defaultCellHeight
-        default:
-            return 0
         }
     }
 
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        let section = indexPath.section
         let row = indexPath.row
-        switch section {
-        case sectionMembers:
-            switch row {
-            case sectionMembersRowAddMember:
-                let cell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath)
-                if let actionCell = cell as? ActionCell {
-                    actionCell.actionTitle = String.localized("group_add_members")
-                    actionCell.actionColor = UIColor.systemBlue
-                }
-                return cell
-            case sectionMembersRowJoinQR:
-                let cell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath)
-                if let actionCell = cell as? ActionCell {
-                    actionCell.actionTitle = String.localized("qrshow_join_group_title")
-                    actionCell.actionColor = UIColor.systemBlue
-                }
-                return cell
-            default:
-                let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath)
-                if let contactCell = cell as? ContactCell {
-                    let contact = groupMembers[row - sectionMembersStaticRowCount]
-                    let displayName = contact.displayName
-                    contactCell.titleLabel.text = displayName
-                    contactCell.subtitleLabel.text = contact.email
-                    contactCell.avatar.setName(displayName)
-                    contactCell.avatar.setColor(contact.color)
-                    if let profileImage = contact.profileImage {
-                        contactCell.avatar.setImage(profileImage)
-                    }
-                    contactCell.setVerified(isVerified: contact.isVerified)
-                }
-                return cell
+        let sectionType = sections[indexPath.section]
+        switch sectionType {
+        case .memberManagement:
+            guard let actionCell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath) as? ActionCell else {
+                safe_fatalError("could not dequeu action cell")
+                break
             }
-        case sectionLeaveGroup:
-            let cell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath)
-            if let actionCell = cell as? ActionCell {
-                actionCell.actionTitle = String.localized("menu_leave_group")
-                actionCell.actionColor = UIColor.red
+            if row == 0 {
+                actionCell.actionTitle = String.localized("group_add_members")
+                actionCell.actionColor = UIColor.systemBlue
+
+            } else {
+                actionCell.actionTitle = String.localized("qrshow_join_group_title")
+                actionCell.actionColor = UIColor.systemBlue
+            }
+            return actionCell
+        case .members:
+            guard let contactCell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as? ContactCell else {
+                safe_fatalError("could not dequeu contactCell cell")
+                break
+
+            }
+            let cellData = ContactCellData(contactId: groupMemberIds[row])
+            let cellViewModel = ContactCellViewModel(contactData: cellData)
+            contactCell.updateCell(cellViewModel: cellViewModel)
+            return contactCell
+        case .chatActions:
+            if row == 0 {
+                return archiveChatCell
+            } else if row == 1 {
+                return leaveGroupCell
+            } else if row == 2 {
+                return deleteChatCell
             }
-            return cell
-        default:
-            return UITableViewCell(frame: .zero)
         }
+        // should never get here
+        return UITableViewCell(frame: .zero)
     }
 
     func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
-        let section = indexPath.section
+        let sectionType = sections[indexPath.section]
         let row = indexPath.row
-        if section == sectionMembers {
-            if row == sectionMembersRowAddMember {
+
+        switch sectionType {
+        case .memberManagement:
+            if row == 0 {
                 coordinator?.showAddGroupMember(chatId: chat.id)
-            } else if row == sectionMembersRowJoinQR {
+            } else if row == 1 {
                 coordinator?.showQrCodeInvite(chatId: chat.id)
-            } else {
-                let contact = getGroupMember(at: row)
-                coordinator?.showContactDetail(of: contact.id)
             }
-        } else if section == sectionLeaveGroup {
-            leaveGroup()
+        case .members:
+            let member = getGroupMember(at: row)
+            coordinator?.showContactDetail(of: member.id)
+        case .chatActions:
+            if row == 0 {
+                toggleArchiveChat()
+            } else if row == 1 {
+                showLeaveGroupConfirmationAlert()
+            } else if row == 2 {
+                showDeleteChatConfirmationAlert()
+            }
         }
     }
 
+    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        if sections[section] == .members {
+            return String.localized("tab_members")
+        }
+        return nil
+    }
+
     func tableView(_: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
-        let section = indexPath.section
+        guard let currentUser = self.currentUser else {
+            return false
+        }
         let row = indexPath.row
-
-        if let currentUser = currentUser {
-            if section == sectionMembers, row >= sectionMembersStaticRowCount, groupMembers[row - sectionMembersStaticRowCount].id != currentUser.id {
-                return true
-            }
+        let sectionType = sections[indexPath.section]
+        if sectionType == .members && groupMemberIds[row] != currentUser.id {
+            return true
         }
         return false
     }
 
     func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
-        let section = indexPath.section
+        guard let currentUser = self.currentUser else {
+            return nil
+        }
         let row = indexPath.row
-
-        // assigning swipe by delete to members (except for current user)
-        if section == sectionMembers, row >= sectionMembersStaticRowCount, groupMembers[row - sectionMembersStaticRowCount].id != currentUser?.id {
+        let sectionType = sections[indexPath.section]
+        if sectionType == .members && groupMemberIds[row] != currentUser.id {
+            // action set for members except for current user
             let delete = UITableViewRowAction(style: .destructive, title: String.localized("remove_desktop")) { [unowned self] _, indexPath in
 
                 let contact = self.getGroupMember(at: row)
@@ -252,24 +284,55 @@ extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSou
                 alert.addAction(UIAlertAction(title: String.localized("remove_desktop"), style: .destructive, handler: { _ in
                     let success = dc_remove_contact_from_chat(mailboxPointer, UInt32(self.chat.id), UInt32(contact.id))
                     if success == 1 {
-                        self.groupMembers.remove(at: row - self.sectionMembersStaticRowCount)
-                        tableView.deleteRows(at: [indexPath], with: .fade)
-                        tableView.reloadData()
+                        self.removeGroupeMemberFromTableAt(indexPath)
                     }
                 }))
                 alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
                 self.present(alert, animated: true, completion: nil)
-            }
+
+ }
             delete.backgroundColor = UIColor.red
             return [delete]
-        } else {
-            return nil
         }
+        return nil
+    }
+
+    private func getGroupMember(at row: Int) -> DcContact {
+        return DcContact(id: groupMemberIds[row])
     }
 
-    func getGroupMember(at row: Int) -> DcContact {
-        let memberId = self.groupMembers[row - self.sectionMembersStaticRowCount].id
-        return DcContact(id: memberId)
+    private func removeGroupeMemberFromTableAt(_ indexPath: IndexPath) {
+        self.groupMemberIds.remove(at: indexPath.row)
+        self.tableView.deleteRows(at: [indexPath], with: .automatic)
+        updateHeader()  // to display correct group size
+    }
+}
+
+// MARK: -alerts
+extension GroupChatDetailViewController {
+    private func showDeleteChatConfirmationAlert() {
+        let alert = UIAlertController(
+            title: nil,
+            message: String.localized("ask_delete_chat_desktop"),
+            preferredStyle: .safeActionSheet
+        )
+        alert.addAction(UIAlertAction(title: String.localized("menu_delete_chat"), style: .destructive, handler: { _ in
+            self.coordinator?.deleteChat()
+        }))
+        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+        self.present(alert, animated: true, completion: nil)
     }
 
+    private func showLeaveGroupConfirmationAlert() {
+        if let userId = currentUser?.id {
+            let alert = UIAlertController(title: String.localized("ask_leave_group"), message: nil, preferredStyle: .safeActionSheet)
+            alert.addAction(UIAlertAction(title: String.localized("menu_leave_group"), style: .destructive, handler: { _ in
+                dc_remove_contact_from_chat(mailboxPointer, UInt32(self.chat.id), UInt32(userId))
+                self.editBarButtonItem.isEnabled = false
+                self.updateGroupMembers()
+            }))
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+            present(alert, animated: true, completion: nil)
+        }
+    }
 }

+ 54 - 11
deltachat-ios/Coordinator/AppCoordinator.swift

@@ -341,11 +341,13 @@ class NewChatCoordinator: Coordinator {
 class GroupChatDetailCoordinator: Coordinator {
     var dcContext: DcContext
     let navigationController: UINavigationController
+    let chatId: Int
 
     private var childCoordinators: [Coordinator] = []
 
-    init(dcContext: DcContext, navigationController: UINavigationController) {
+    init(dcContext: DcContext, chatId: Int, navigationController: UINavigationController) {
         self.dcContext = dcContext
+        self.chatId = chatId
         self.navigationController = navigationController
     }
 
@@ -387,6 +389,35 @@ class GroupChatDetailCoordinator: Coordinator {
         navigationController.pushViewController(contactDetailController, animated: true)
     }
 
+    func deleteChat() {
+        /*
+        app will navigate to chatlist or archive and delete the chat there
+        notify chatList/archiveList to delete chat AFTER is is visible
+        */
+        func notifyToDeleteChat() {
+            NotificationCenter.default.post(name: dcNotificationChatDeletedInChatDetail, object: nil, userInfo: ["chat_id": self.chatId])
+        }
+
+        func showArchive() {
+            self.navigationController.popToRootViewController(animated: false) // in main ChatList now
+            let controller = ChatListController(dcContext: dcContext, showArchive: true)
+            let coordinator = ChatListCoordinator(dcContext: dcContext, navigationController: navigationController)
+            childCoordinators.append(coordinator)
+            controller.coordinator = coordinator
+            navigationController.pushViewController(controller, animated: false)
+        }
+
+        CATransaction.begin()
+        CATransaction.setCompletionBlock(notifyToDeleteChat)
+
+        let chat = DcChat(id: chatId)
+        if chat.isArchived {
+            showArchive()
+        } else {
+            self.navigationController.popToRootViewController(animated: true) // in main ChatList now
+        }
+        CATransaction.commit()
+    }
 }
 
 class ChatViewCoordinator: NSObject, Coordinator {
@@ -422,8 +453,8 @@ class ChatViewCoordinator: NSObject, Coordinator {
                 navigationController.pushViewController(contactDetailController, animated: true)
             }
         case .GROUP, .VERYFIEDGROUP:
-            let groupChatDetailViewController = GroupChatDetailViewController(chatId: chatId) // inherits from ChatDetailViewController
-            let coordinator = GroupChatDetailCoordinator(dcContext: dcContext, navigationController: navigationController)
+            let groupChatDetailViewController = GroupChatDetailViewController(chatId: chatId, context: dcContext) // inherits from ChatDetailViewController
+            let coordinator = GroupChatDetailCoordinator(dcContext: dcContext, chatId: chatId, navigationController: navigationController)
             childCoordinators.append(coordinator)
             groupChatDetailViewController.coordinator = coordinator
             navigationController.pushViewController(groupChatDetailViewController, animated: true)
@@ -602,20 +633,33 @@ class ContactDetailCoordinator: Coordinator, ContactDetailCoordinatorProtocol {
             return
         }
 
+        /*
+        app will navigate to chatlist or archive and delete the chat there
+        notify chatList/archiveList to delete chat AFTER is is visible
+        */
         func notifyToDeleteChat() {
             NotificationCenter.default.post(name: dcNotificationChatDeletedInChatDetail, object: nil, userInfo: ["chat_id": chatId])
         }
 
-        // we want to notify chatList to delete chat AFTER is is visible
+        func showArchive() {
+            self.navigationController.popToRootViewController(animated: false) // in main ChatList now
+            let controller = ChatListController(dcContext: dcContext, showArchive: true)
+            let coordinator = ChatListCoordinator(dcContext: dcContext, navigationController: navigationController)
+            childCoordinators.append(coordinator)
+            controller.coordinator = coordinator
+            navigationController.pushViewController(controller, animated: false)
+        }
+
         CATransaction.begin()
-        CATransaction.setAnimationDuration(2)
         CATransaction.setCompletionBlock(notifyToDeleteChat)
-        self.navigationController.popToRootViewController(animated: true)
-        CATransaction.commit()
-    }
 
-    func archiveChat() {
-        print("archive chat")
+        let chat = DcChat(id: chatId)
+        if chat.isArchived {
+            showArchive()
+        } else {
+            self.navigationController.popToRootViewController(animated: true) // in main ChatList now
+        }
+        CATransaction.commit()
     }
 
 }
@@ -674,7 +718,6 @@ protocol ContactDetailCoordinatorProtocol: class {
     func showEditContact(contactId: Int)
     func showChat(chatId: Int)
     func deleteChat()
-    func archiveChat()
 }
 
 protocol EditContactCoordinatorProtocol: class {

+ 2 - 2
deltachat-ios/DC/Wrapper.swift

@@ -67,7 +67,7 @@ class DcContext {
     }
 
     func archiveChat(chatId: Int, archive: Bool) {
-        dc_archive_chat(contextPointer, UInt32(chatId), Int32(archive ? 1 : 0))
+        dc_set_chat_visibility(contextPointer, UInt32(chatId), Int32(archive ? DC_CHAT_VISIBILITY_ARCHIVED : DC_CHAT_VISIBILITY_NORMAL))
     }
 
     func marknoticedChat(chatId: Int) {
@@ -472,7 +472,7 @@ class DcChat {
     }
 
     var isArchived: Bool {
-        return Int(dc_chat_get_archived(chatPointer)) == 1
+        return Int(dc_chat_get_visibility(chatPointer)) == DC_CHAT_VISIBILITY_ARCHIVED
     }
 
     var isUnpromoted: Bool {

+ 2 - 2
deltachat-ios/Handler/DeviceContactsHandler.swift

@@ -23,7 +23,7 @@ class DeviceContactsHandler {
     }
 
     private func addContactsToCore() {
-        fetchContactsWithEmailFromDevice() { contacts in
+        fetchContactsWithEmailFromDevice { contacts in
             DispatchQueue.main.async {
                 let contactString = self.makeContactString(contacts: contacts)
                 self.dcContext.addContacts(contactString: contactString)
@@ -32,7 +32,7 @@ class DeviceContactsHandler {
         }
     }
 
-    private func fetchContactsWithEmailFromDevice(completionHandler: @escaping ([CNContact])->Void) {
+    private func fetchContactsWithEmailFromDevice(completionHandler: @escaping ([CNContact]) -> Void) {
 
         DispatchQueue.global(qos: .background).async {
             let keys = [CNContactFamilyNameKey, CNContactGivenNameKey, CNContactEmailAddressesKey]

+ 75 - 3
deltachat-ios/View/ContactDetailHeader.swift

@@ -1,19 +1,91 @@
 import UIKit
 
-class ContactDetailHeader: ContactCell {
+class ContactDetailHeader: UIView {
+
+    public static let headerHeight: CGFloat = 74.5
+
+    let badgeSize: CGFloat = 54
+
+    private lazy var avatar: InitialsBadge = {
+        let badge = InitialsBadge(size: badgeSize)
+        badge.setColor(UIColor.lightGray)
+        badge.isAccessibilityElement = false
+        return badge
+    }()
+
+    private var titleLabel: UILabel = {
+        let label = UILabel()
+        label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
+        label.lineBreakMode = .byTruncatingTail
+        label.textColor = DcColors.defaultTextColor
+        label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1), for: NSLayoutConstraint.Axis.horizontal)
+        return label
+    }()
+
+    private var subtitleLabel: UILabel = {
+        let label = UILabel()
+        label.font = UIFont.systemFont(ofSize: 14)
+        label.textColor = UIColor(hexString: "848ba7")
+        label.lineBreakMode = .byTruncatingTail
+        return label
+    }()
+
     init() {
-        super.init(style: .default, reuseIdentifier: nil)
+        super.init(frame: .zero)
         let bg = UIColor(red: 248 / 255, green: 248 / 255, blue: 255 / 255, alpha: 1.0)
         backgroundColor = bg
-        selectionStyle = .none
+        setupSubviews()
     }
 
     required init?(coder _: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }
 
+    private func setupSubviews() {
+        let margin: CGFloat = 10
+        let verticalStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
+
+        addSubview(avatar)
+        addSubview(verticalStackView)
+
+        avatar.translatesAutoresizingMaskIntoConstraints = false
+        verticalStackView.translatesAutoresizingMaskIntoConstraints = false
+
+        addConstraints([
+            avatar.constraintWidthTo(badgeSize),
+            avatar.constraintHeightTo(badgeSize),
+            avatar.constraintAlignLeadingTo(self, paddingLeading: badgeSize / 4),
+            avatar.constraintCenterYTo(self),
+        ])
+
+
+        verticalStackView.clipsToBounds = true
+        verticalStackView.leadingAnchor.constraint(equalTo: avatar.trailingAnchor, constant: margin).isActive = true
+        verticalStackView.centerYAnchor.constraint(equalTo: avatar.centerYAnchor).isActive = true
+        verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -margin).isActive = true
+        verticalStackView.axis = .vertical
+    }
+
     func updateDetails(title: String?, subtitle: String?) {
         titleLabel.text = title
         subtitleLabel.text = subtitle
     }
+
+    func setImage(_ image: UIImage) {
+        avatar.setImage(image)
+    }
+
+    func resetBackupImage() {
+        avatar.setColor(UIColor.clear)
+        avatar.setName("")
+    }
+
+    func setBackupImage(name: String, color: UIColor) {
+        avatar.setColor(color)
+        avatar.setName(name)
+    }
+
+    func setVerified(isVerified: Bool) {
+        avatar.setVerified(isVerified)
+    }
 }

+ 1 - 1
deltachat-ios/ViewModel/ContactCellViewModel.swift

@@ -54,7 +54,7 @@ class ContactCellViewModel: AvatarCellViewModel {
     }
 }
 
-class ChatCellViewModel: AvatarCellViewModel{
+class ChatCellViewModel: AvatarCellViewModel {
 
     private let chat: DcChat
     private let summary: DcLot

+ 37 - 8
deltachat-ios/ViewModel/ContactDetailViewModel.swift

@@ -6,28 +6,37 @@ protocol ContactDetailViewModelProtocol {
     var numberOfSections: Int { get }
     var chatIsArchived: Bool { get }
     func numberOfRowsInSection(_ : Int) -> Int
-    func typeFor(section: Int) -> ContactDetailViewModel.SectionType
+    func typeFor(section: Int) -> ContactDetailViewModel.ProfileSections
+    func actionFor(row: Int) -> ContactDetailViewModel.ChatAction
     func update(sharedChatCell: ContactCell, row index: Int)
     func getSharedChatIdAt(indexPath: IndexPath) -> Int
     func titleFor(section: Int) -> String?
+    func toggleArchiveChat() -> Bool // returns true if chat is archived after action
 }
 
 class ContactDetailViewModel: ContactDetailViewModelProtocol {
 
     let context: DcContext
-    enum SectionType {
+    enum ProfileSections {
         case startChat
         case sharedChats
         case chatActions //  archive chat, block chat, delete chats
     }
 
+    enum ChatAction {
+        case archiveChat
+        case blockChat
+        case deleteChat
+    }
+
     var contactId: Int
 
     var contact: DcContact
     private let chatId: Int?
     private let sharedChats: DcChatlist
     private let startChatOption: Bool
-    private var sections: [SectionType] = []
+    private var sections: [ProfileSections] = []
+    private var actions: [ChatAction] = [] // chatDetail: archive, block, delete - else: block
 
     /// if chatId is nil this is a contact detail with 'start chat'-option
     init(contactId: Int, chatId: Int?, context: DcContext) {
@@ -45,15 +54,26 @@ class ContactDetailViewModel: ContactDetailViewModelProtocol {
             sections.append(.sharedChats)
         }
         sections.append(.chatActions)
+
+        if chatId != nil {
+            actions = [.archiveChat, .blockChat, .deleteChat]
+        } else {
+            actions = [.blockChat]
+        }
+
     }
 
-    func typeFor(section: Int) -> ContactDetailViewModel.SectionType {
+    func typeFor(section: Int) -> ContactDetailViewModel.ProfileSections {
         return sections[section]
     }
 
+    func actionFor(row: Int) -> ContactDetailViewModel.ChatAction {
+        return actions[row]
+    }
+
     var chatIsArchived: Bool {
         guard let chatId = chatId else {
-            safe_fatalError("This is a ContactDetail view with no chat id")
+           // safe_fatalError("This is a ContactDetail view with no chat id")
             return false
         }
         return DcChat(id: chatId).isArchived
@@ -67,7 +87,7 @@ class ContactDetailViewModel: ContactDetailViewModelProtocol {
         switch sections[section] {
         case .sharedChats: return sharedChats.length
         case .startChat: return 1
-        case .chatActions: return 3
+        case .chatActions: return actions.count
         }
     }
 
@@ -89,8 +109,17 @@ class ContactDetailViewModel: ContactDetailViewModelProtocol {
 
     func titleFor(section: Int) -> String? {
         if sections[section] == .sharedChats {
-           return String.localized("profile_shared_chats")
+            return String.localized("profile_shared_chats")
         }
         return nil
-      }
+    }
+
+    func toggleArchiveChat() -> Bool {
+        guard let chatId = chatId else {
+            safe_fatalError("there is no chatId - you are probably are calling this from ContactDetail - this should be only called from ChatDetail")
+            return false
+        }
+        context.archiveChat(chatId: chatId, archive: !chatIsArchived)
+        return chatIsArchived
+    }
 }