Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions FlowCrypt.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,40 @@ import Foundation

protocol ContactDetailDecoratorType {
var title: String { get }
func nodeInput(with contact: Contact) -> ContactDetailNode.Input
func userNodeInput(with contact: RecipientWithPubKeys) -> ContactUserCellNode.Input
func keyNodeInput(with key: PubKey) -> ContactKeyCellNode.Input
}

struct ContactDetailDecorator: ContactDetailDecoratorType {
let title = "contact_detail_screen_title".localized

func nodeInput(with contact: Contact) -> ContactDetailNode.Input {
func userNodeInput(with contact: RecipientWithPubKeys) -> ContactUserCellNode.Input {
ContactUserCellNode.Input(
user: (contact.name ?? contact.email).attributed(.regular(16))
)
}

func keyNodeInput(with key: PubKey) -> ContactKeyCellNode.Input {
let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .medium

let fingerpringString = key.fingerprint ?? "-"

let createdString: String = {
if let created = contact.pubkeyCreated {
let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .medium
return df.string(from: created)
} else {
return "-"
}
guard let created = key.created else { return "-" }
return df.string(from: created)
}()

let expiresString: String = {
guard let expires = key.expiresOn else { return "never" }
return df.string(from: expires)
}()
return ContactDetailNode.Input(
user: (contact.name ?? contact.email).attributed(.regular(16)),
ids: contact.longids.joined(separator: ",\n").attributed(.regular(14)),
fingerprints: contact.fingerprints.joined(separator: ",\n").attributed(.regular(14)),
algoInfo: contact.algo?.algorithm.attributed(.regular(14)),
created: createdString.attributed(.regular(14))

return ContactKeyCellNode.Input(
fingerprint: fingerpringString.attributed(.regular(13)),
createdAt: createdString.attributed(.regular(14)),
expires: expiresString.attributed(.regular(14))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,38 @@
//

import AsyncDisplayKit
import FlowCryptCommon
import FlowCryptUI

/**
* View controller which shows details about a contact and the public key recorded for it
* View controller which shows details about a contact and lists public keys recorded for it
* - User can be redirected here from settings *ContactsListViewController* by tapping on a particular contact
*/
final class ContactDetailViewController: TableNodeViewController {
typealias ContactDetailAction = (Action) -> Void

enum Action {
case delete(_ contact: Contact)
case delete(_ recipient: RecipientWithPubKeys)
}

private enum Section: Int, CaseIterable {
case header = 0, keys
}

private let decorator: ContactDetailDecoratorType
private let contact: Contact
private let contactsProvider: LocalContactsProviderType
private var recipient: RecipientWithPubKeys
private let action: ContactDetailAction?

init(
decorator: ContactDetailDecoratorType = ContactDetailDecorator(),
contact: Contact,
contactsProvider: LocalContactsProviderType = LocalContactsProvider(),
recipient: RecipientWithPubKeys,
action: ContactDetailAction?
) {
self.decorator = decorator
self.contact = contact
self.contactsProvider = contactsProvider
self.recipient = recipient
self.action = action
super.init(node: TableNode())
}
Expand All @@ -51,45 +59,97 @@ final class ContactDetailViewController: TableNodeViewController {
private func setupNavigationBarItems() {
navigationItem.rightBarButtonItem = NavigationBarItemsView(
with: [
.init(image: UIImage(named: "share"), action: (self, #selector(handleSaveAction))),
.init(image: UIImage(named: "copy"), action: (self, #selector(handleCopyAction))),
.init(image: UIImage(named: "trash"), action: (self, #selector(handleRemoveAction)))
.init(image: UIImage(systemName: "trash"), action: (self, #selector(handleRemoveAction)))
]
)
}
}

extension ContactDetailViewController {
@objc private final func handleSaveAction() {
let vc = UIActivityViewController(
activityItems: [contact.pubKey],
applicationActivities: nil
)
present(vc, animated: true, completion: nil)
}

@objc private final func handleCopyAction() {
UIPasteboard.general.string = contact.pubKey
showToast("contact_detail_copy".localized)
}

@objc private final func handleRemoveAction() {
navigationController?.popViewController(animated: true) { [weak self] in
guard let self = self else { return }
self.action?(.delete(self.contact))
self.action?(.delete(self.recipient))
}
}

private func delete(with context: Either<PubKey, IndexPath>) {
let keyToRemove: PubKey
let indexPathToRemove: IndexPath
switch context {
case .left(let key):
keyToRemove = key
guard let index = recipient.pubKeys.firstIndex(where: { $0 == key }) else {
assertionFailure("Can't find index of the contact")
return
}
indexPathToRemove = IndexPath(row: index, section: 1)
case .right(let indexPath):
indexPathToRemove = indexPath
keyToRemove = recipient.pubKeys[indexPath.row]
}

recipient.remove(pubKey: keyToRemove)
if let fingerprint = keyToRemove.fingerprint, fingerprint.isNotEmpty {
contactsProvider.removePubKey(with: fingerprint, for: recipient.email)
}
node.deleteRows(at: [indexPathToRemove], with: .left)
}
}

extension ContactDetailViewController: ASTableDelegate, ASTableDataSource {
func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int {
1
func numberOfSections(in tableNode: ASTableNode) -> Int {
Section.allCases.count
}

func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int {
guard let section = Section(rawValue: section) else { return 0 }

switch section {
case .header: return 1
case .keys: return recipient.pubKeys.count
}
}

func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
{ [weak self] in
guard let self = self else { return ASCellNode() }
return ContactDetailNode(input: self.decorator.nodeInput(with: self.contact))
guard let self = self, let section = Section(rawValue: indexPath.section)
else { return ASCellNode() }
return self.node(for: section, row: indexPath.row)
}
}

func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) {
guard let section = Section(rawValue: indexPath.section) else { return }

switch section {
case .header:
return
case .keys:
let pubKey = recipient.pubKeys[indexPath.row]
let contactKeyDetailViewController = ContactKeyDetailViewController(pubKey: pubKey) { [weak self] action in
guard case let .delete(key) = action else {
assertionFailure("Action is not implemented")
return
}
self?.delete(with: .left(key))
}

navigationController?.pushViewController(contactKeyDetailViewController, animated: true)
}
}
}

// MARK: - UI
extension ContactDetailViewController {
private func node(for section: Section, row: Int) -> ASCellNode {
switch section {
case .header:
return ContactUserCellNode(input: self.decorator.userNodeInput(with: self.recipient))
case .keys:
return ContactKeyCellNode(
input: self.decorator.keyNodeInput(with: self.recipient.pubKeys[row])
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// ContactKeyDetailDecorator.swift
// FlowCrypt
//
// Created by Roma Sosnovsky on 13/10/21
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import FlowCryptUI
import Foundation

protocol ContactKeyDetailDecoratorType {
var title: String { get }
func attributedTitle(for contactKeyPart: ContactKeyDetailViewController.Part) -> NSAttributedString
}

struct ContactKeyDetailDecorator: ContactKeyDetailDecoratorType {
let title = "contact_key_detail_screen_title".localized

func attributedTitle(for contactKeyPart: ContactKeyDetailViewController.Part) -> NSAttributedString {
let title: String
switch contactKeyPart {
case .key: title = "contact_key_pub".localized
case .signature: title = "contact_key_signature".localized
case .created: title = "contact_key_created".localized
case .checked: title = "contact_key_fetched".localized
case .expire: title = "contact_key_expires".localized
case .longids: title = "contact_key_longids".localized
case .fingerprints: title = "contact_key_fingerprints".localized
case .algo: title = "contact_key_algo".localized
}

return title.attributed(.bold(16))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// ContactKeyDetailViewController.swift
// FlowCrypt
//
// Created by Roma Sosnovsky on 13/10/21
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import AsyncDisplayKit
import FlowCryptCommon
import FlowCryptUI

/**
* View controller which shows details about contact's public key
* - User can be redirected here from *ContactDetailViewController* by tapping on a particular key
*/
final class ContactKeyDetailViewController: TableNodeViewController {
typealias ContactKeyDetailAction = (Action) -> Void

enum Action {
case delete(_ key: PubKey)
}

enum Part: Int, CaseIterable {
case key = 0, signature, created, checked, expire, longids, fingerprints, algo
}

private let decorator: ContactKeyDetailDecoratorType
private let pubKey: PubKey
private let action: ContactKeyDetailAction?

init(
decorator: ContactKeyDetailDecoratorType = ContactKeyDetailDecorator(),
pubKey: PubKey,
action: ContactKeyDetailAction?
) {
self.decorator = decorator
self.pubKey = pubKey
self.action = action
super.init(node: TableNode())
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
node.delegate = self
node.dataSource = self
title = decorator.title
setupNavigationBarItems()
}

private func setupNavigationBarItems() {
navigationItem.rightBarButtonItem = NavigationBarItemsView(
with: [
.init(image: UIImage(named: "share"), action: (self, #selector(handleSaveAction))),
.init(image: UIImage(named: "copy"), action: (self, #selector(handleCopyAction))),
.init(image: UIImage(systemName: "trash"), action: (self, #selector(handleRemoveAction)))
]
)
}
}

extension ContactKeyDetailViewController {
@objc private final func handleSaveAction() {
let vc = UIActivityViewController(
activityItems: [pubKey.key],
applicationActivities: nil
)
present(vc, animated: true, completion: nil)
}

@objc private final func handleCopyAction() {
UIPasteboard.general.string = pubKey.key
showToast("contact_detail_copy".localized)
}

@objc private final func handleRemoveAction() {
navigationController?.popViewController(animated: true) { [weak self] in
guard let self = self else { return }
self.action?(.delete(self.pubKey))
}
}
}

extension ContactKeyDetailViewController: ASTableDelegate, ASTableDataSource {
func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int {
Part.allCases.count
}

func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
{ [weak self] in
guard let self = self, let part = Part(rawValue: indexPath.row)
else { return ASCellNode() }
return self.node(for: part)
}
}
}

// MARK: - UI
extension ContactKeyDetailViewController {
private func node(for part: Part) -> ASCellNode {
LabelCellNode(title: decorator.attributedTitle(for: part),
text: content(for: part).attributed(.regular(14)))
}

private func content(for part: Part) -> String {
switch part {
case .key:
return pubKey.key
case .signature:
return string(from: pubKey.lastSig)
case .created:
return string(from: pubKey.created)
case .checked:
return string(from: pubKey.lastChecked)
case .expire:
return string(from: pubKey.expiresOn)
case .longids:
return pubKey.longids.joined(separator: ", ")
case .fingerprints:
return pubKey.fingerprints.joined(separator: ", ")
case .algo:
return pubKey.algo?.algorithm ?? "-"
}
}

private func string(from date: Date?) -> String {
guard let date = date else { return "-" }

let df = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .medium
return df.string(from: date)
}
}
Loading