Refactor DeviceVerification prefix classes to KeyVerification where needed. And move key verification related classes into the same module KeyVerification.

This commit is contained in:
SBiOSoftWhare
2020-03-18 17:39:26 +01:00
parent 991e2fe5f7
commit 23a02c362c
91 changed files with 819 additions and 803 deletions
@@ -0,0 +1,75 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
import UIKit
final class UserVerificationSessionStatusCoordinator: UserVerificationSessionStatusCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let userId: String
private let deviceId: String
private var userVerificationSessionStatusViewModel: UserVerificationSessionStatusViewModelType
private let userVerificationSessionStatusViewController: UserVerificationSessionStatusViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: UserVerificationSessionStatusCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, userId: String, userDisplayName: String?, deviceId: String) {
self.session = session
self.userId = userId
self.deviceId = deviceId
let userVerificationSessionStatusViewModel = UserVerificationSessionStatusViewModel(session: self.session, userId: userId, userDisplayName: userDisplayName, deviceId: deviceId)
let userVerificationSessionStatusViewController = UserVerificationSessionStatusViewController.instantiate(with: userVerificationSessionStatusViewModel)
self.userVerificationSessionStatusViewModel = userVerificationSessionStatusViewModel
self.userVerificationSessionStatusViewController = userVerificationSessionStatusViewController
}
// MARK: - Public methods
func start() {
self.userVerificationSessionStatusViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.userVerificationSessionStatusViewController
}
}
// MARK: - UserVerificationSessionStatusViewModelCoordinatorDelegate
extension UserVerificationSessionStatusCoordinator: UserVerificationSessionStatusViewModelCoordinatorDelegate {
func userVerificationSessionStatusViewModel(_ viewModel: UserVerificationSessionStatusViewModelType, wantsToManuallyVerifyDeviceWithId deviceId: String, for userId: String) {
self.delegate?.userVerificationSessionStatusCoordinator(self, wantsToManuallyVerifyDeviceWithId: deviceId, for: userId)
}
func userVerificationSessionStatusViewModelDidClose(_ viewModel: UserVerificationSessionStatusViewModelType) {
self.delegate?.userVerificationSessionStatusCoordinatorDidClose(self)
}
}
@@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationSessionStatusCoordinatorDelegate: class {
func userVerificationSessionStatusCoordinator(_ coordinator: UserVerificationSessionStatusCoordinatorType, wantsToManuallyVerifyDeviceWithId deviceId: String, for userId: String)
func userVerificationSessionStatusCoordinatorDidClose(_ coordinator: UserVerificationSessionStatusCoordinatorType)
}
/// `UserVerificationSessionStatusCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
protocol UserVerificationSessionStatusCoordinatorType: Coordinator, Presentable {
var delegate: UserVerificationSessionStatusCoordinatorDelegate? { get }
}
@@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// UserVerificationSessionStatusViewController view actions exposed to view model
enum UserVerificationSessionStatusViewAction {
case loadData
case verify
case close
}
@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="edG-Ef-G5W">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--User Verification Session Status View Controller-->
<scene sceneID="yMy-34-3pS">
<objects>
<viewController id="edG-Ef-G5W" customClass="UserVerificationSessionStatusViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="G1A-RF-WrC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="16P-bA-3wP">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sX2-Xu-9f5">
<rect key="frame" x="0.0" y="0.0" width="375" height="296.5"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1lx-d3-6c2">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Qdf-kG-wgx">
<rect key="frame" x="325" y="20" width="30" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Pon-L2-rt1"/>
<constraint firstAttribute="width" constant="30" id="qav-Na-EDh"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<state key="normal" image="close_button"/>
<connections>
<action selector="closeButtonAction:" destination="edG-Ef-G5W" eventType="touchUpInside" id="0qe-Df-Nw1"/>
</connections>
</button>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="encryption_warning" translatesAutoresizingMaskIntoConstraints="NO" id="0d0-9z-5Ar">
<rect key="frame" x="20" y="23" width="24" height="24"/>
<constraints>
<constraint firstAttribute="width" secondItem="0d0-9z-5Ar" secondAttribute="height" multiplier="1:1" id="EYK-FL-Zx6"/>
<constraint firstAttribute="height" constant="24" id="MuJ-Ex-1Vk"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Warning" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mXI-Sg-wYe">
<rect key="frame" x="54" y="23" width="261" height="24"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Messages with this user in this room are end-to-end encrypted and cant be read by third parties." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CTY-XM-iLf">
<rect key="frame" x="20" y="62" width="335" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="Qdf-kG-wgx" secondAttribute="trailing" constant="20" id="2JS-ui-NNI"/>
<constraint firstItem="Qdf-kG-wgx" firstAttribute="leading" secondItem="mXI-Sg-wYe" secondAttribute="trailing" constant="10" id="7VI-oy-V0w"/>
<constraint firstItem="CTY-XM-iLf" firstAttribute="top" secondItem="mXI-Sg-wYe" secondAttribute="bottom" constant="15" id="8UG-sf-OQU"/>
<constraint firstItem="mXI-Sg-wYe" firstAttribute="centerY" secondItem="Qdf-kG-wgx" secondAttribute="centerY" id="Ad5-Kd-ox8"/>
<constraint firstItem="0d0-9z-5Ar" firstAttribute="centerY" secondItem="mXI-Sg-wYe" secondAttribute="centerY" id="Jlp-rC-N2E"/>
<constraint firstItem="CTY-XM-iLf" firstAttribute="trailing" secondItem="Qdf-kG-wgx" secondAttribute="trailing" id="LK6-M8-kCT"/>
<constraint firstAttribute="bottom" secondItem="CTY-XM-iLf" secondAttribute="bottom" constant="10" id="MrU-Ar-zkT"/>
<constraint firstItem="mXI-Sg-wYe" firstAttribute="leading" secondItem="0d0-9z-5Ar" secondAttribute="trailing" constant="10" id="O4U-Wa-zdT"/>
<constraint firstItem="CTY-XM-iLf" firstAttribute="leading" secondItem="0d0-9z-5Ar" secondAttribute="leading" id="UeO-oR-wzQ"/>
<constraint firstItem="Qdf-kG-wgx" firstAttribute="top" secondItem="1lx-d3-6c2" secondAttribute="top" constant="20" id="WOL-w8-7T3"/>
<constraint firstItem="0d0-9z-5Ar" firstAttribute="leading" secondItem="1lx-d3-6c2" secondAttribute="leading" constant="20" id="cMk-cs-f9i"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3Mu-7j-flw">
<rect key="frame" x="0.0" y="108" width="375" height="40.5"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="encryption_warning" translatesAutoresizingMaskIntoConstraints="NO" id="0ME-4K-MWA" customClass="encryption_warning">
<rect key="frame" x="20" y="13.5" width="14" height="14"/>
<constraints>
<constraint firstAttribute="width" secondItem="0ME-4K-MWA" secondAttribute="height" multiplier="1:1" id="r0c-Ix-lm4"/>
<constraint firstAttribute="width" constant="14" id="zKC-ZT-6y8"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Riot iOS (BLPOWKUPGQ)" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FET-ZT-Q69">
<rect key="frame" x="44" y="10" width="311" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="0ME-4K-MWA" firstAttribute="leading" secondItem="3Mu-7j-flw" secondAttribute="leading" constant="20" id="8W6-Zu-Qu2"/>
<constraint firstAttribute="trailing" secondItem="FET-ZT-Q69" secondAttribute="trailing" constant="20" id="BbB-ge-jNu"/>
<constraint firstItem="FET-ZT-Q69" firstAttribute="top" secondItem="3Mu-7j-flw" secondAttribute="top" constant="10" id="Bw3-IL-AwO"/>
<constraint firstAttribute="bottom" secondItem="FET-ZT-Q69" secondAttribute="bottom" constant="10" id="NGo-OD-AHt"/>
<constraint firstItem="FET-ZT-Q69" firstAttribute="leading" secondItem="0ME-4K-MWA" secondAttribute="trailing" constant="10" id="Sap-sa-3tW"/>
<constraint firstItem="0ME-4K-MWA" firstAttribute="centerY" secondItem="FET-ZT-Q69" secondAttribute="centerY" id="W4V-Dz-CKf"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="rB7-Ct-Mq7">
<rect key="frame" x="0.0" y="148.5" width="375" height="148"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oRN-W6-TVF">
<rect key="frame" x="0.0" y="0.0" width="375" height="148"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dd2-N5-GEx">
<rect key="frame" x="20" y="10" width="335" height="54"/>
<string key="text">Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it.</string>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HiI-uV-Hjw">
<rect key="frame" x="20" y="84" width="335" height="44"/>
<accessibility key="accessibilityConfiguration" identifier="AuthenticationVCLoginButton"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="LKd-e3-0AI"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="exF-8e-73K"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
<inset key="contentEdgeInsets" minX="30" minY="0.0" maxX="30" maxY="0.0"/>
<state key="normal" title="Manually verify">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="verifyButtonAction:" destination="edG-Ef-G5W" eventType="touchUpInside" id="gGx-OT-X5w"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="HiI-uV-Hjw" secondAttribute="bottom" constant="20" id="3bP-Hs-m42"/>
<constraint firstAttribute="trailing" secondItem="dd2-N5-GEx" secondAttribute="trailing" constant="20" id="BHw-Yv-ceG"/>
<constraint firstItem="dd2-N5-GEx" firstAttribute="leading" secondItem="oRN-W6-TVF" secondAttribute="leading" constant="20" id="BV7-Vu-PQR"/>
<constraint firstItem="HiI-uV-Hjw" firstAttribute="top" secondItem="dd2-N5-GEx" secondAttribute="bottom" constant="20" id="BtT-Mg-Okq"/>
<constraint firstItem="HiI-uV-Hjw" firstAttribute="centerX" secondItem="oRN-W6-TVF" secondAttribute="centerX" id="Ign-Yp-tpB"/>
<constraint firstItem="dd2-N5-GEx" firstAttribute="top" secondItem="oRN-W6-TVF" secondAttribute="top" constant="10" id="Ygn-tl-ahK"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="HiI-uV-Hjw" secondAttribute="trailing" constant="20" id="nqA-lS-ubx"/>
<constraint firstItem="HiI-uV-Hjw" firstAttribute="width" secondItem="oRN-W6-TVF" secondAttribute="width" priority="250" id="qp1-z5-eP9"/>
<constraint firstItem="HiI-uV-Hjw" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="oRN-W6-TVF" secondAttribute="leading" constant="20" id="uNl-Ok-uee"/>
<constraint firstAttribute="height" priority="250" id="yuI-ch-y92"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="oRN-W6-TVF" firstAttribute="width" secondItem="rB7-Ct-Mq7" secondAttribute="width" id="2pR-3D-RYU"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="3Mu-7j-flw" secondAttribute="trailing" id="W3d-d0-w2v"/>
<constraint firstItem="rB7-Ct-Mq7" firstAttribute="leading" secondItem="sX2-Xu-9f5" secondAttribute="leading" id="aH4-6p-pgC"/>
<constraint firstItem="1lx-d3-6c2" firstAttribute="top" secondItem="sX2-Xu-9f5" secondAttribute="top" id="aTB-di-K6F"/>
<constraint firstAttribute="bottom" secondItem="rB7-Ct-Mq7" secondAttribute="bottom" id="dOS-zD-hAw"/>
<constraint firstAttribute="trailing" secondItem="rB7-Ct-Mq7" secondAttribute="trailing" id="iH7-HJ-6RG"/>
<constraint firstAttribute="trailing" secondItem="1lx-d3-6c2" secondAttribute="trailing" id="lLi-mF-pmE"/>
<constraint firstItem="1lx-d3-6c2" firstAttribute="leading" secondItem="sX2-Xu-9f5" secondAttribute="leading" id="rU8-aC-b2j"/>
<constraint firstItem="3Mu-7j-flw" firstAttribute="leading" secondItem="sX2-Xu-9f5" secondAttribute="leading" id="thK-xG-Pvw"/>
<constraint firstItem="3Mu-7j-flw" firstAttribute="top" secondItem="1lx-d3-6c2" secondAttribute="bottom" id="xjU-4b-Ac0"/>
<constraint firstItem="rB7-Ct-Mq7" firstAttribute="top" secondItem="3Mu-7j-flw" secondAttribute="bottom" id="yOc-gk-ilO"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="sX2-Xu-9f5" secondAttribute="trailing" id="9uF-gg-w2a"/>
<constraint firstItem="sX2-Xu-9f5" firstAttribute="width" secondItem="16P-bA-3wP" secondAttribute="width" id="AbT-qq-n6g"/>
<constraint firstItem="sX2-Xu-9f5" firstAttribute="leading" secondItem="16P-bA-3wP" secondAttribute="leading" id="pP0-dc-iec"/>
<constraint firstAttribute="bottom" secondItem="sX2-Xu-9f5" secondAttribute="bottom" id="tUz-rr-k5B"/>
<constraint firstItem="sX2-Xu-9f5" firstAttribute="top" secondItem="16P-bA-3wP" secondAttribute="top" id="yrj-Cm-1Zm"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="16P-bA-3wP" secondAttribute="bottom" id="3Ak-zJ-1Ca"/>
<constraint firstItem="16P-bA-3wP" firstAttribute="top" secondItem="cZY-v6-GjO" secondAttribute="top" id="SV3-Ai-4QS"/>
<constraint firstItem="16P-bA-3wP" firstAttribute="leading" secondItem="cZY-v6-GjO" secondAttribute="leading" id="dlk-85-Zca"/>
<constraint firstItem="16P-bA-3wP" firstAttribute="trailing" secondItem="cZY-v6-GjO" secondAttribute="trailing" id="hFM-5p-Ne1"/>
</constraints>
<viewLayoutGuide key="safeArea" id="cZY-v6-GjO"/>
</view>
<connections>
<outlet property="badgeImageView" destination="0d0-9z-5Ar" id="M0i-8Q-Zal"/>
<outlet property="closeButton" destination="Qdf-kG-wgx" id="yl7-lg-gW6"/>
<outlet property="deviceInformationLabel" destination="FET-ZT-Q69" id="D8n-5b-H4p"/>
<outlet property="deviceStatusImageView" destination="0ME-4K-MWA" id="kgp-dT-Q0P"/>
<outlet property="informationLabel" destination="CTY-XM-iLf" id="qVC-lS-YOY"/>
<outlet property="titleLabel" destination="mXI-Sg-wYe" id="Wrj-QM-srt"/>
<outlet property="untrustedSessionContainerView" destination="oRN-W6-TVF" id="9sc-53-MWY"/>
<outlet property="untrustedSessionInformationLabel" destination="dd2-N5-GEx" id="6TB-Ve-OyC"/>
<outlet property="verifyButton" destination="HiI-uV-Hjw" id="JnR-0Q-aSc"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8Fe-S5-Pf4" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-175.19999999999999" y="124.58770614692655"/>
</scene>
</scenes>
<resources>
<image name="close_button" width="16" height="16"/>
<image name="encryption_warning" width="16" height="16"/>
</resources>
</document>
@@ -0,0 +1,270 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
final class UserVerificationSessionStatusViewController: UIViewController {
// MARK: - Constants
private enum Constants {
static let verifyButtonCornerRadius: CGFloat = 8.0
static let informationTextDefaultFont = UIFont.systemFont(ofSize: 15.0)
static let informationTextBoldFont = UIFont.systemFont(ofSize: 15.0, weight: .medium)
static let deviceNameFont = UIFont.systemFont(ofSize: 17.0, weight: .medium)
static let deviceIdFont = UIFont.systemFont(ofSize: 15.0)
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var badgeImageView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var informationLabel: UILabel!
@IBOutlet private weak var deviceStatusImageView: UIImageView!
@IBOutlet private weak var deviceInformationLabel: UILabel!
@IBOutlet private weak var untrustedSessionContainerView: UIView!
@IBOutlet private weak var untrustedSessionInformationLabel: UILabel!
@IBOutlet private weak var verifyButton: UIButton!
// MARK: Private
private var viewModel: UserVerificationSessionStatusViewModelType!
private var theme: Theme!
private var keyboardAvoider: KeyboardAvoider?
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
// MARK: - Setup
class func instantiate(with viewModel: UserVerificationSessionStatusViewModelType) -> UserVerificationSessionStatusViewController {
let viewController = StoryboardScene.UserVerificationSessionStatusViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.setupViews()
self.vc_removeBackTitle()
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.verifyButton.layer.cornerRadius = Constants.verifyButtonCornerRadius
self.closeButton.layer.cornerRadius = self.closeButton.frame.size.width/2
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
self.view.backgroundColor = theme.headerBackgroundColor
self.titleLabel.textColor = theme.textPrimaryColor
self.closeButton.vc_setBackgroundColor(theme.headerTextSecondaryColor, for: .normal)
self.informationLabel.textColor = theme.textPrimaryColor
self.untrustedSessionInformationLabel.textColor = theme.textPrimaryColor
self.verifyButton.vc_setBackgroundColor(theme.tintColor, for: .normal)
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupViews() {
self.closeButton.layer.masksToBounds = true
self.verifyButton.layer.masksToBounds = true
}
private func render(viewState: UserVerificationSessionStatusViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded(viewData: let sessionStatusViewData):
self.renderLoaded(viewData: sessionStatusViewData)
case .error(let error):
self.render(error: error)
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(viewData: SessionStatusViewData) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
let badgeImage: UIImage
let title: String
self.untrustedSessionContainerView.isHidden = viewData.isDeviceTrusted
if viewData.isDeviceTrusted {
badgeImage = Asset.Images.encryptionTrusted.image
title = VectorL10n.userVerificationSessionDetailsTrustedTitle
} else {
badgeImage = Asset.Images.encryptionWarning.image
title = VectorL10n.userVerificationSessionDetailsUntrustedTitle
}
let unstrustedInformationText: String
let verifyButtonTitle: String
if viewData.isCurrentUser {
unstrustedInformationText = VectorL10n.userVerificationSessionDetailsAdditionalInformationUntrustedCurrentUser
verifyButtonTitle = VectorL10n.userVerificationSessionDetailsVerifyActionCurrentUser
} else {
unstrustedInformationText = VectorL10n.userVerificationSessionDetailsAdditionalInformationUntrustedOtherUser
verifyButtonTitle = VectorL10n.userVerificationSessionDetailsVerifyActionOtherUser
}
self.badgeImageView.image = badgeImage
self.titleLabel.text = title
self.informationLabel.attributedText = self.buildInformationAttributedText(with: viewData)
self.deviceStatusImageView.image = badgeImage
self.deviceInformationLabel.attributedText = self.builDeviceInfoAttributedText(with: viewData)
self.untrustedSessionInformationLabel.text = unstrustedInformationText
self.verifyButton.setTitle(verifyButtonTitle, for: .normal)
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
private func buildUserInfoText(with userId: String, userDisplayName: String?) -> String {
let userInfoText: String
if let userDisplayName = userDisplayName {
userInfoText = "\(userDisplayName) (\(userId))"
} else {
userInfoText = userId
}
return userInfoText
}
private func buildInformationAttributedText(with viewData: SessionStatusViewData) -> NSAttributedString {
let informationAttributedText: NSMutableAttributedString = NSMutableAttributedString()
let informationTextDefaultAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: self.theme.textPrimaryColor,
.font: Constants.informationTextDefaultFont]
let informationTextBoldAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: self.theme.textPrimaryColor,
.font: Constants.informationTextBoldFont]
let userInfoText = self.buildUserInfoText(with: viewData.userId, userDisplayName: viewData.userDisplayName)
if viewData.isDeviceTrusted {
if viewData.isCurrentUser {
let informationAttributedStringPart1 = NSAttributedString(string: VectorL10n.userVerificationSessionDetailsInformationTrustedCurrentUser, attributes: informationTextDefaultAttributes)
informationAttributedText.append(informationAttributedStringPart1)
} else {
let informationAttributedStringPart1 = NSAttributedString(string: VectorL10n.userVerificationSessionDetailsInformationTrustedOtherUserPart1, attributes: informationTextDefaultAttributes)
let informationAttributedStringPart2 = NSAttributedString(string: userInfoText, attributes: informationTextBoldAttributes)
let informationAttributedStringPart3 = NSAttributedString(string: VectorL10n.userVerificationSessionDetailsInformationTrustedOtherUserPart2, attributes: informationTextDefaultAttributes)
informationAttributedText.append(informationAttributedStringPart1)
informationAttributedText.append(informationAttributedStringPart2)
informationAttributedText.append(informationAttributedStringPart3)
}
} else {
if viewData.isCurrentUser {
let informationAttributedStringPart1 = NSAttributedString(string: VectorL10n.userVerificationSessionDetailsInformationUntrustedCurrentUser, attributes: informationTextDefaultAttributes)
informationAttributedText.append(informationAttributedStringPart1)
} else {
let informationAttributedStringPart1 = NSAttributedString(string: userInfoText, attributes: informationTextBoldAttributes)
let informationAttributedStringPart2 = NSAttributedString(string: VectorL10n.userVerificationSessionDetailsInformationUntrustedOtherUser, attributes: informationTextDefaultAttributes)
informationAttributedText.append(informationAttributedStringPart1)
informationAttributedText.append(informationAttributedStringPart2)
}
}
return informationAttributedText
}
private func builDeviceInfoAttributedText(with viewData: SessionStatusViewData) -> NSAttributedString {
let deviceInfoAttributedText = NSMutableAttributedString()
let deviceInfoAttributedTextPart1 = NSAttributedString(string: "\(viewData.deviceName) ", attributes: [.foregroundColor: self.theme.textPrimaryColor, .font: Constants.deviceNameFont])
let deviceInfoAttributedTextPart2 = NSAttributedString(string: "(\(viewData.deviceId))", attributes: [.foregroundColor: self.theme.textSecondaryColor, .font: Constants.deviceIdFont])
deviceInfoAttributedText.append(deviceInfoAttributedTextPart1)
deviceInfoAttributedText.append(deviceInfoAttributedTextPart2)
return deviceInfoAttributedText
}
// MARK: - Actions
@IBAction private func closeButtonAction(_ sender: Any) {
self.viewModel.process(viewAction: .close)
}
@IBAction private func verifyButtonAction(_ sender: Any) {
self.viewModel.process(viewAction: .verify)
}
}
// MARK: - UserVerificationSessionStatusViewModelViewDelegate
extension UserVerificationSessionStatusViewController: UserVerificationSessionStatusViewModelViewDelegate {
func userVerificationSessionStatusViewModel(_ viewModel: UserVerificationSessionStatusViewModelType, didUpdateViewState viewSate: UserVerificationSessionStatusViewState) {
self.render(viewState: viewSate)
}
}
@@ -0,0 +1,95 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
struct SessionStatusViewData {
let userId: String
let userDisplayName: String?
let isCurrentUser: Bool
let deviceId: String
let deviceName: String
let isDeviceTrusted: Bool
}
enum UserVerificationSessionStatusViewModelError: Error {
case deviceNotFound
}
final class UserVerificationSessionStatusViewModel: UserVerificationSessionStatusViewModelType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let userId: String
private let userDisplayName: String?
private let deviceId: String
// MARK: Public
weak var viewDelegate: UserVerificationSessionStatusViewModelViewDelegate?
weak var coordinatorDelegate: UserVerificationSessionStatusViewModelCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, userId: String, userDisplayName: String?, deviceId: String) {
self.session = session
self.userId = userId
self.userDisplayName = userDisplayName
self.deviceId = deviceId
}
// MARK: - Public
func process(viewAction: UserVerificationSessionStatusViewAction) {
switch viewAction {
case .loadData:
self.loadData()
case .verify:
self.coordinatorDelegate?.userVerificationSessionStatusViewModel(self, wantsToManuallyVerifyDeviceWithId: self.deviceId, for: self.userId)
case .close:
self.coordinatorDelegate?.userVerificationSessionStatusViewModelDidClose(self)
}
}
// MARK: - Private
private func loadData() {
guard let deviceInfo = self.session.crypto.device(withDeviceId: self.deviceId, ofUser: self.userId) else {
self.update(viewState: .error(UserVerificationSessionStatusViewModelError.deviceNotFound))
return
}
let isCurrentUser = self.session.myUser.userId == self.userId
let viewData = SessionStatusViewData(userId: self.userId,
userDisplayName: self.userDisplayName,
isCurrentUser: isCurrentUser,
deviceId: deviceInfo.deviceId,
deviceName: deviceInfo.displayName ?? "",
isDeviceTrusted: deviceInfo.trustLevel.isVerified)
self.update(viewState: .loaded(viewData: viewData))
}
private func update(viewState: UserVerificationSessionStatusViewState) {
self.viewDelegate?.userVerificationSessionStatusViewModel(self, didUpdateViewState: viewState)
}
}
@@ -0,0 +1,37 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationSessionStatusViewModelViewDelegate: class {
func userVerificationSessionStatusViewModel(_ viewModel: UserVerificationSessionStatusViewModelType, didUpdateViewState viewSate: UserVerificationSessionStatusViewState)
}
protocol UserVerificationSessionStatusViewModelCoordinatorDelegate: class {
func userVerificationSessionStatusViewModel(_ viewModel: UserVerificationSessionStatusViewModelType, wantsToManuallyVerifyDeviceWithId deviceId: String, for userId: String)
func userVerificationSessionStatusViewModelDidClose(_ viewModel: UserVerificationSessionStatusViewModelType)
}
/// Protocol describing the view model used by `UserVerificationSessionStatusViewController`
protocol UserVerificationSessionStatusViewModelType {
var viewDelegate: UserVerificationSessionStatusViewModelViewDelegate? { get set }
var coordinatorDelegate: UserVerificationSessionStatusViewModelCoordinatorDelegate? { get set }
func process(viewAction: UserVerificationSessionStatusViewAction)
}
@@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh SessionStatus UserVerificationSessionStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// UserVerificationSessionStatusViewController view state
enum UserVerificationSessionStatusViewState {
case loading
case loaded(viewData: SessionStatusViewData)
case error(Error)
}
@@ -0,0 +1,79 @@
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
import Reusable
struct UserVerificationSessionStatusViewData {
let deviceId: String
let sessionName: String
let isTrusted: Bool
}
final class UserVerificationSessionStatusCell: UITableViewCell, NibReusable, Themable {
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var statusImageView: UIImageView!
@IBOutlet private weak var sessionNameLabel: UILabel!
@IBOutlet private weak var statusTextLabel: UILabel!
// MARK: Private
private var viewData: UserVerificationSessionStatusViewData?
private var theme: Theme?
// MARK: - Public
func fill(viewData: UserVerificationSessionStatusViewData) {
self.viewData = viewData
let statusText: String
let statusImage: UIImage
if viewData.isTrusted {
statusImage = Asset.Images.encryptionTrusted.image
statusText = VectorL10n.userVerificationSessionsListSessionTrusted
} else {
statusImage = Asset.Images.encryptionWarning.image
statusText = VectorL10n.userVerificationSessionsListSessionUntrusted
}
self.statusImageView.image = statusImage
self.statusTextLabel.text = statusText
self.sessionNameLabel.text = viewData.sessionName
self.updateStatusTextColor()
}
func update(theme: Theme) {
self.theme = theme
self.backgroundColor = theme.headerBackgroundColor
self.sessionNameLabel.textColor = theme.textPrimaryColor
self.updateStatusTextColor()
}
// MARK: - Private
private func updateStatusTextColor() {
guard let viewData = self.viewData, let theme = self.theme else {
return
}
self.statusTextLabel.textColor = viewData.isTrusted ? theme.tintColor : theme.warningColor
}
}
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="37" id="KGk-i7-Jjw" customClass="UserVerificationSessionStatusCell" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="307" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="307" height="36.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="V5p-pi-DJj">
<rect key="frame" x="0.0" y="0.0" width="307" height="36.5"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="encryption_warning" translatesAutoresizingMaskIntoConstraints="NO" id="4Ja-9R-o9i" customClass="encryption_warning">
<rect key="frame" x="20" y="11.5" width="14" height="14"/>
<constraints>
<constraint firstAttribute="width" secondItem="4Ja-9R-o9i" secondAttribute="height" multiplier="1:1" id="Ivd-S1-mib"/>
<constraint firstAttribute="width" constant="14" id="n0o-Xb-nnn"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Riot iOS" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MkZ-9n-kCS">
<rect key="frame" x="44" y="10" width="174.5" height="16.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" text="Trusted" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wsp-qe-DLy">
<rect key="frame" x="228.5" y="8" width="58.5" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.01176470588" green="0.70196078429999997" blue="0.50588235290000005" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="MkZ-9n-kCS" firstAttribute="leading" secondItem="4Ja-9R-o9i" secondAttribute="trailing" constant="10" id="29O-OV-c6O"/>
<constraint firstItem="4Ja-9R-o9i" firstAttribute="leading" secondItem="V5p-pi-DJj" secondAttribute="leading" constant="20" id="Gcu-EC-b72"/>
<constraint firstItem="wsp-qe-DLy" firstAttribute="centerY" secondItem="MkZ-9n-kCS" secondAttribute="centerY" id="JQR-uR-OSm"/>
<constraint firstAttribute="bottom" secondItem="MkZ-9n-kCS" secondAttribute="bottom" constant="10" id="Ohk-Zx-SaA"/>
<constraint firstAttribute="trailing" secondItem="wsp-qe-DLy" secondAttribute="trailing" constant="20" id="Onn-ew-tSg"/>
<constraint firstItem="4Ja-9R-o9i" firstAttribute="centerY" secondItem="MkZ-9n-kCS" secondAttribute="centerY" id="U88-nG-bpa"/>
<constraint firstItem="wsp-qe-DLy" firstAttribute="leading" secondItem="MkZ-9n-kCS" secondAttribute="trailing" constant="10" id="VdB-e0-Ru9"/>
<constraint firstItem="MkZ-9n-kCS" firstAttribute="top" secondItem="V5p-pi-DJj" secondAttribute="top" constant="10" id="lx8-Ae-MX8"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="V5p-pi-DJj" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="3rQ-mA-tnY"/>
<constraint firstItem="V5p-pi-DJj" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="PDK-oM-MhJ"/>
<constraint firstAttribute="bottom" secondItem="V5p-pi-DJj" secondAttribute="bottom" id="dj0-s9-48N"/>
<constraint firstAttribute="trailing" secondItem="V5p-pi-DJj" secondAttribute="trailing" id="lp6-TZ-ycQ"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="sessionNameLabel" destination="MkZ-9n-kCS" id="atH-sc-sJx"/>
<outlet property="statusImageView" destination="4Ja-9R-o9i" id="n6o-LV-b3O"/>
<outlet property="statusTextLabel" destination="wsp-qe-DLy" id="4dV-vY-pmR"/>
</connections>
<point key="canvasLocation" x="-72" y="26"/>
</tableViewCell>
</objects>
<resources>
<image name="encryption_warning" width="16" height="16"/>
</resources>
</document>
@@ -0,0 +1,70 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
import UIKit
final class UserVerificationSessionsStatusCoordinator: UserVerificationSessionsStatusCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private var userVerificationSessionsStatusViewModel: UserVerificationSessionsStatusViewModelType
private let userVerificationSessionsStatusViewController: UserVerificationSessionsStatusViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: UserVerificationSessionsStatusCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, userId: String) {
self.session = session
let userVerificationSessionsStatusViewModel = UserVerificationSessionsStatusViewModel(session: self.session, userId: userId)
let userVerificationSessionsStatusViewController = UserVerificationSessionsStatusViewController.instantiate(with: userVerificationSessionsStatusViewModel)
self.userVerificationSessionsStatusViewModel = userVerificationSessionsStatusViewModel
self.userVerificationSessionsStatusViewController = userVerificationSessionsStatusViewController
}
// MARK: - Public methods
func start() {
self.userVerificationSessionsStatusViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.userVerificationSessionsStatusViewController
}
}
// MARK: - UserVerificationSessionsStatusViewModelCoordinatorDelegate
extension UserVerificationSessionsStatusCoordinator: UserVerificationSessionsStatusViewModelCoordinatorDelegate {
func userVerificationSessionsStatusViewModel(_ viewModel: UserVerificationSessionsStatusViewModelType, didSelectDeviceWithId deviceId: String, for userId: String) {
self.delegate?.userVerificationSessionsStatusCoordinator(self, didSelectDeviceWithId: deviceId, for: userId)
}
func userVerificationSessionsStatusViewModelDidClose(_ viewModel: UserVerificationSessionsStatusViewModelType) {
self.delegate?.userVerificationSessionsStatusCoordinatorDidClose(self)
}
}
@@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationSessionsStatusCoordinatorDelegate: class {
func userVerificationSessionsStatusCoordinatorDidClose(_ coordinator: UserVerificationSessionsStatusCoordinatorType)
func userVerificationSessionsStatusCoordinator(_ coordinator: UserVerificationSessionsStatusCoordinatorType, didSelectDeviceWithId deviceId: String, for userId: String)
}
/// `UserVerificationSessionsStatusCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
protocol UserVerificationSessionsStatusCoordinatorType: Coordinator, Presentable {
var delegate: UserVerificationSessionsStatusCoordinatorDelegate? { get }
}
@@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// UserVerificationSessionsStatusViewController view actions exposed to view model
enum UserVerificationSessionsStatusViewAction {
case loadData
case selectSession(deviceId: String)
case close
}
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="le3-e8-C1h">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--User Verification Sessions Status View Controller-->
<scene sceneID="G1j-dD-ArC">
<objects>
<viewController id="le3-e8-C1h" customClass="UserVerificationSessionsStatusViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="IXd-J2-Vex">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kfr-Ho-krK">
<rect key="frame" x="0.0" y="20" width="375" height="108"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NBJ-A1-IP0">
<rect key="frame" x="325" y="20" width="30" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="30" id="jda-sE-4Uz"/>
<constraint firstAttribute="height" constant="30" id="rPb-uQ-Tyt"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<state key="normal" image="close_button"/>
<connections>
<action selector="closeButtonAction:" destination="le3-e8-C1h" eventType="touchUpInside" id="4JW-Rl-hIk"/>
</connections>
</button>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="encryption_warning" translatesAutoresizingMaskIntoConstraints="NO" id="rZH-h1-WTw">
<rect key="frame" x="20" y="23" width="24" height="24"/>
<constraints>
<constraint firstAttribute="width" secondItem="rZH-h1-WTw" secondAttribute="height" multiplier="1:1" id="RgU-aV-cmS"/>
<constraint firstAttribute="height" constant="24" id="v0F-Dp-CmF"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Warning" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fcs-Vl-AtB">
<rect key="frame" x="54" y="23" width="261" height="24"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Messages with this user in this room are end-to-end encrypted and cant be read by third parties." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vfc-7p-LLs">
<rect key="frame" x="20" y="62" width="335" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="NBJ-A1-IP0" firstAttribute="leading" secondItem="fcs-Vl-AtB" secondAttribute="trailing" constant="10" id="0cd-68-5tR"/>
<constraint firstItem="fcs-Vl-AtB" firstAttribute="leading" secondItem="rZH-h1-WTw" secondAttribute="trailing" constant="10" id="75Z-TB-Hhj"/>
<constraint firstItem="fcs-Vl-AtB" firstAttribute="centerY" secondItem="NBJ-A1-IP0" secondAttribute="centerY" id="DVH-2n-tXo"/>
<constraint firstItem="Vfc-7p-LLs" firstAttribute="trailing" secondItem="NBJ-A1-IP0" secondAttribute="trailing" id="KQg-I1-5FP"/>
<constraint firstItem="rZH-h1-WTw" firstAttribute="centerY" secondItem="fcs-Vl-AtB" secondAttribute="centerY" id="N8y-sA-Am7"/>
<constraint firstItem="NBJ-A1-IP0" firstAttribute="top" secondItem="Kfr-Ho-krK" secondAttribute="top" constant="20" id="VUJ-Jj-UjR"/>
<constraint firstAttribute="trailing" secondItem="NBJ-A1-IP0" secondAttribute="trailing" constant="20" id="ZH0-2n-XKy"/>
<constraint firstItem="rZH-h1-WTw" firstAttribute="leading" secondItem="Kfr-Ho-krK" secondAttribute="leading" constant="20" id="gPw-ja-veq"/>
<constraint firstItem="Vfc-7p-LLs" firstAttribute="top" secondItem="fcs-Vl-AtB" secondAttribute="bottom" constant="15" id="qh5-nR-1EN"/>
<constraint firstAttribute="bottom" secondItem="Vfc-7p-LLs" secondAttribute="bottom" constant="10" id="t5D-ht-cSc"/>
<constraint firstItem="Vfc-7p-LLs" firstAttribute="leading" secondItem="rZH-h1-WTw" secondAttribute="leading" id="zAt-yf-7HQ"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sessions" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uMA-Dt-xTz">
<rect key="frame" x="20" y="133" width="335" height="24"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="drr-Gd-5wA">
<rect key="frame" x="0.0" y="167" width="375" height="500"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="dataSource" destination="le3-e8-C1h" id="Xmr-jN-S8s"/>
<outlet property="delegate" destination="le3-e8-C1h" id="Y5J-nA-Ynp"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="drr-Gd-5wA" firstAttribute="leading" secondItem="IXd-J2-Vex" secondAttribute="leading" id="52r-4B-JdK"/>
<constraint firstItem="Kfr-Ho-krK" firstAttribute="trailing" secondItem="Eud-3V-Vqw" secondAttribute="trailing" id="9Zg-EV-cHL"/>
<constraint firstItem="Eud-3V-Vqw" firstAttribute="trailing" secondItem="uMA-Dt-xTz" secondAttribute="trailing" constant="20" id="Amb-5F-Ors"/>
<constraint firstAttribute="trailing" secondItem="drr-Gd-5wA" secondAttribute="trailing" id="Awf-hM-uzJ"/>
<constraint firstItem="drr-Gd-5wA" firstAttribute="top" secondItem="uMA-Dt-xTz" secondAttribute="bottom" constant="10" id="H1a-sI-AbP"/>
<constraint firstAttribute="bottom" secondItem="drr-Gd-5wA" secondAttribute="bottom" id="Nhh-Ps-Upr"/>
<constraint firstItem="Kfr-Ho-krK" firstAttribute="top" secondItem="Eud-3V-Vqw" secondAttribute="top" id="Wxs-Uw-xoA"/>
<constraint firstItem="uMA-Dt-xTz" firstAttribute="leading" secondItem="Eud-3V-Vqw" secondAttribute="leading" constant="20" id="an1-BC-BP0"/>
<constraint firstItem="uMA-Dt-xTz" firstAttribute="top" secondItem="Kfr-Ho-krK" secondAttribute="bottom" constant="5" id="wok-Oq-W0z"/>
<constraint firstItem="Eud-3V-Vqw" firstAttribute="leading" secondItem="Kfr-Ho-krK" secondAttribute="leading" id="xjg-JA-ZUT"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Eud-3V-Vqw"/>
</view>
<connections>
<outlet property="badgeImageImageView" destination="rZH-h1-WTw" id="M2a-7i-OoW"/>
<outlet property="closeButton" destination="NBJ-A1-IP0" id="AYv-b7-fFp"/>
<outlet property="informationLabel" destination="Vfc-7p-LLs" id="em9-jE-1wo"/>
<outlet property="sessionsTableViewTitle" destination="uMA-Dt-xTz" id="vFU-UV-jMO"/>
<outlet property="tableView" destination="drr-Gd-5wA" id="Cs0-pF-wwB"/>
<outlet property="titleLabel" destination="fcs-Vl-AtB" id="iGp-kn-QfJ"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dBA-Zm-evE" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-463.19999999999999" y="-80.50974512743629"/>
</scene>
</scenes>
<resources>
<image name="close_button" width="16" height="16"/>
<image name="encryption_warning" width="16" height="16"/>
</resources>
</document>
@@ -0,0 +1,234 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
final class UserVerificationSessionsStatusViewController: UIViewController {
// MARK: - Constants
private enum Constants {
static let estimatedRowHeight: CGFloat = 40.0
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var badgeImageImageView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var informationLabel: UILabel!
@IBOutlet private weak var sessionsTableViewTitle: UILabel!
@IBOutlet private weak var tableView: UITableView!
// MARK: Private
private var viewModel: UserVerificationSessionsStatusViewModelType!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityIndicatorPresenter: ActivityIndicatorPresenter!
private var sessionsStatusViewData: [UserVerificationSessionStatusViewData] = []
private var userEncryptionTrustLevel: UserEncryptionTrustLevel = .unknown
// MARK: - Setup
class func instantiate(with viewModel: UserVerificationSessionsStatusViewModelType) -> UserVerificationSessionsStatusViewController {
let viewController = StoryboardScene.UserVerificationSessionsStatusViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.setupViews()
self.vc_removeBackTitle()
self.activityIndicatorPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.closeButton.layer.cornerRadius = self.closeButton.frame.size.width/2
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
self.tableView.deselectRow(at: selectedIndexPath, animated: animated)
}
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.headerBackgroundColor
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
self.closeButton.vc_setBackgroundColor(theme.headerTextSecondaryColor, for: .normal)
self.titleLabel.textColor = theme.textPrimaryColor
self.informationLabel.textColor = theme.textPrimaryColor
self.sessionsTableViewTitle.textColor = theme.textPrimaryColor
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupViews() {
self.closeButton.layer.masksToBounds = true
self.setupTableView()
self.updateTitleViews()
self.sessionsTableViewTitle.text = VectorL10n.userVerificationSessionsListTableTitle
self.informationLabel.text = VectorL10n.userVerificationSessionsListInformation
}
private func setupTableView() {
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = Constants.estimatedRowHeight
self.tableView.separatorStyle = .none
self.tableView.tableFooterView = UIView()
self.tableView.alwaysBounceVertical = false
self.tableView.register(cellType: UserVerificationSessionStatusCell.self)
}
private func render(viewState: UserVerificationSessionsStatusViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded(userTrustLevel: let userTrustLevel, sessionsStatusViewData: let sessionsStatusViewData):
self.renderLoaded(userTrustLevel: userTrustLevel, sessionsStatusViewData: sessionsStatusViewData)
case .error(let error):
self.render(error: error)
}
}
private func renderLoading() {
self.tableView.isUserInteractionEnabled = false
self.activityIndicatorPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(userTrustLevel: UserEncryptionTrustLevel, sessionsStatusViewData: [UserVerificationSessionStatusViewData]) {
self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true)
self.tableView.isUserInteractionEnabled = true
self.userEncryptionTrustLevel = userTrustLevel
self.sessionsStatusViewData = sessionsStatusViewData
self.updateTitleViews()
self.tableView.reloadData()
}
private func render(error: Error) {
self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true)
self.tableView.isUserInteractionEnabled = true
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
private func updateTitleViews() {
let badgeImage: UIImage
let title: String
switch self.userEncryptionTrustLevel {
case .trusted:
badgeImage = Asset.Images.encryptionTrusted.image
title = VectorL10n.userVerificationSessionsListUserTrustLevelTrustedTitle
case .warning:
badgeImage = Asset.Images.encryptionWarning.image
title = VectorL10n.userVerificationSessionsListUserTrustLevelWarningTitle
default:
badgeImage = Asset.Images.encryptionNormal.image
title = VectorL10n.userVerificationSessionsListUserTrustLevelUnknownTitle
}
self.badgeImageImageView.image = badgeImage
self.titleLabel.text = title
}
// MARK: - Actions
@IBAction private func closeButtonAction(_ sender: Any) {
self.viewModel.process(viewAction: .close)
}
}
// MARK: - UITableViewDataSource
extension UserVerificationSessionsStatusViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sessionsStatusViewData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: UserVerificationSessionStatusCell.self)
let viewData = self.sessionsStatusViewData[indexPath.row]
cell.update(theme: self.theme)
cell.fill(viewData: viewData)
return cell
}
}
// MARK: - UITableViewDelegate
extension UserVerificationSessionsStatusViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let viewData = self.sessionsStatusViewData[indexPath.row]
self.viewModel.process(viewAction: .selectSession(deviceId: viewData.deviceId))
}
}
// MARK: - UserVerificationSessionsStatusViewModelViewDelegate
extension UserVerificationSessionsStatusViewController: UserVerificationSessionsStatusViewModelViewDelegate {
func userVerificationSessionsStatusViewModel(_ viewModel: UserVerificationSessionsStatusViewModelType, didUpdateViewState viewSate: UserVerificationSessionsStatusViewState) {
self.render(viewState: viewSate)
}
}
@@ -0,0 +1,148 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
enum UserVerificationSessionsStatusViewModelError: Error {
case unknown
}
final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsStatusViewModelType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let userId: String
private var currentOperation: MXHTTPOperation?
private var userTrustLevel: UserEncryptionTrustLevel
// MARK: Public
weak var viewDelegate: UserVerificationSessionsStatusViewModelViewDelegate?
weak var coordinatorDelegate: UserVerificationSessionsStatusViewModelCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, userId: String) {
self.session = session
self.userId = userId
self.userTrustLevel = .unknown
}
deinit {
self.currentOperation?.cancel()
}
// MARK: - Public
func process(viewAction: UserVerificationSessionsStatusViewAction) {
switch viewAction {
case .loadData:
self.loadData()
case .selectSession(deviceId: let deviceId):
self.coordinatorDelegate?.userVerificationSessionsStatusViewModel(self, didSelectDeviceWithId: deviceId, for: self.userId)
case .close:
self.coordinatorDelegate?.userVerificationSessionsStatusViewModelDidClose(self)
}
}
// MARK: - Private
private func loadData() {
let sessionsStatusViewData = self.getSessionStatusViewDataListFromCache(for: self.userId)
self.update(viewState: .loaded(userTrustLevel: self.userTrustLevel, sessionsStatusViewData: sessionsStatusViewData))
self.fetchSessionStatus()
}
private func update(viewState: UserVerificationSessionsStatusViewState) {
self.viewDelegate?.userVerificationSessionsStatusViewModel(self, didUpdateViewState: viewState)
}
private func fetchSessionStatus() {
self.update(viewState: .loading)
self.currentOperation = self.getSessionStatusViewDataList(for: self.userId) { result in
switch result {
case .success(let sessionsStatusViewData):
let isUserTrusted = sessionsStatusViewData.contains(where: { sessionsStatusViewData -> Bool in
return sessionsStatusViewData.isTrusted == false
}) == false
let userTrustLevel: UserEncryptionTrustLevel = isUserTrusted ? .trusted : .warning
self.update(viewState: .loaded(userTrustLevel: userTrustLevel, sessionsStatusViewData: sessionsStatusViewData))
case .failure(let error):
self.update(viewState: .error(error))
}
}
}
private func getSessionStatusViewDataListFromCache(for userId: String) -> [UserVerificationSessionStatusViewData] {
let deviceInfoList = self.getDevicesFromCache(for: self.userId)
return self.sessionStatusViewDataList(from: deviceInfoList)
}
private func getDevicesFromCache(for userId: String) -> [MXDeviceInfo] {
guard let deviceInfoMap = self.session.crypto.devices(forUser: self.userId) else {
return []
}
return Array(deviceInfoMap.values)
}
@discardableResult
private func getSessionStatusViewDataList(for userId: String, completion: @escaping (Result<[UserVerificationSessionStatusViewData], Error>) -> Void) -> MXHTTPOperation? {
let httpOperation: MXHTTPOperation?
httpOperation = self.session.crypto.downloadKeys([self.userId], forceDownload: false, success: { ( usersDeviceMap: MXUsersDevicesMap<MXDeviceInfo>?, usersCrossSigningMap: [String : MXCrossSigningInfo]?) in
let sessionsViewData: [UserVerificationSessionStatusViewData]
if let usersDeviceMap = usersDeviceMap, let userDeviceInfoMap = Array(usersDeviceMap.map.values).first {
let deviceInfoList = Array(userDeviceInfoMap.values)
sessionsViewData = self.sessionStatusViewDataList(from: deviceInfoList)
} else {
sessionsViewData = []
}
completion(.success(sessionsViewData))
}, failure: { error in
let finalError = error ?? UserVerificationSessionsStatusViewModelError.unknown
completion(.failure(finalError))
})
return httpOperation
}
private func sessionStatusViewData(from deviceInfo: MXDeviceInfo) -> UserVerificationSessionStatusViewData {
return UserVerificationSessionStatusViewData(deviceId: deviceInfo.deviceId, sessionName: deviceInfo.displayName ?? "", isTrusted: deviceInfo.trustLevel.isVerified)
}
private func sessionStatusViewDataList(from deviceInfoList: [MXDeviceInfo]) -> [UserVerificationSessionStatusViewData] {
return deviceInfoList.map { (deviceInfo) -> UserVerificationSessionStatusViewData in
return self.sessionStatusViewData(from: deviceInfo)
}
}
}
@@ -0,0 +1,37 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationSessionsStatusViewModelViewDelegate: class {
func userVerificationSessionsStatusViewModel(_ viewModel: UserVerificationSessionsStatusViewModelType, didUpdateViewState viewSate: UserVerificationSessionsStatusViewState)
}
protocol UserVerificationSessionsStatusViewModelCoordinatorDelegate: class {
func userVerificationSessionsStatusViewModel(_ viewModel: UserVerificationSessionsStatusViewModelType, didSelectDeviceWithId deviceId: String, for userId: String)
func userVerificationSessionsStatusViewModelDidClose(_ viewModel: UserVerificationSessionsStatusViewModelType)
}
/// Protocol describing the view model used by `UserVerificationSessionsStatusViewController`
protocol UserVerificationSessionsStatusViewModelType {
var viewDelegate: UserVerificationSessionsStatusViewModelViewDelegate? { get set }
var coordinatorDelegate: UserVerificationSessionsStatusViewModelCoordinatorDelegate? { get set }
func process(viewAction: UserVerificationSessionsStatusViewAction)
}
@@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh UserVerification UserVerificationSessionsStatus
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// UserVerificationSessionsStatusViewController view state
enum UserVerificationSessionsStatusViewState {
case loading
case loaded(userTrustLevel: UserEncryptionTrustLevel, sessionsStatusViewData: [UserVerificationSessionStatusViewData])
case error(Error)
}
@@ -0,0 +1,82 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
import UIKit
final class UserVerificationStartCoordinator: UserVerificationStartCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomMember: MXRoomMember
private var userVerificationStartViewModel: UserVerificationStartViewModelType
private let userVerificationStartViewController: UserVerificationStartViewController
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: UserVerificationStartCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, roomMember: MXRoomMember) {
self.session = session
self.roomMember = roomMember
let userVerificationStartViewModel = UserVerificationStartViewModel(session: self.session, roomMember: self.roomMember)
let userVerificationStartViewController = UserVerificationStartViewController.instantiate(with: userVerificationStartViewModel)
self.userVerificationStartViewModel = userVerificationStartViewModel
self.userVerificationStartViewController = userVerificationStartViewController
}
// MARK: - Public methods
func start() {
self.userVerificationStartViewModel.coordinatorDelegate = self
}
func toPresentable() -> UIViewController {
return self.userVerificationStartViewController
}
}
// MARK: - UserVerificationStartViewModelCoordinatorDelegate
extension UserVerificationStartCoordinator: UserVerificationStartViewModelCoordinatorDelegate {
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, otherDidAcceptRequest request: MXKeyVerificationRequest) {
self.delegate?.userVerificationStartCoordinator(self, otherDidAcceptRequest: request)
}
func userVerificationStartViewModelDidCancel(_ viewModel: UserVerificationStartViewModelType) {
self.delegate?.userVerificationStartCoordinatorDidCancel(self)
}
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, didCompleteWithIncomingTransaction transaction: MXSASTransaction) {
self.delegate?.userVerificationStartCoordinator(self, didCompleteWithOutgoingTransaction: transaction)
}
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, didTransactionCancelled transaction: MXSASTransaction) {
self.delegate?.userVerificationStartCoordinator(self, didTransactionCancelled: transaction)
}
}
@@ -0,0 +1,35 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationStartCoordinatorDelegate: class {
func userVerificationStartCoordinator(_ coordinator: UserVerificationStartCoordinatorType, otherDidAcceptRequest request: MXKeyVerificationRequest)
func userVerificationStartCoordinator(_ coordinator: UserVerificationStartCoordinatorType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction)
func userVerificationStartCoordinator(_ coordinator: UserVerificationStartCoordinatorType, didTransactionCancelled transaction: MXSASTransaction)
func userVerificationStartCoordinatorDidCancel(_ coordinator: UserVerificationStartCoordinatorType)
}
/// `UserVerificationStartCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
protocol UserVerificationStartCoordinatorType: Coordinator, Presentable {
var delegate: UserVerificationStartCoordinatorDelegate? { get }
}
@@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// UserVerificationStartViewController view actions exposed to view model
enum UserVerificationStartViewAction {
case loadData
case startVerification
case cancel
}
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--User Verification Start View Controller-->
<scene sceneID="mt5-wz-YKA">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="UserVerificationStartViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dQD-Vs-7h6">
<rect key="frame" x="0.0" y="254" width="375" height="179"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="For extra security, verify @user:matrix.org by checking a one-time code on both your devices." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="b2K-LP-jqv">
<rect key="frame" x="20" y="10" width="335" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Waiting for User…" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0QC-yi-0hN">
<rect key="frame" x="20" y="78" width="335" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3sZ-h5-05D">
<rect key="frame" x="20" y="66" width="335" height="44"/>
<accessibility key="accessibilityConfiguration" identifier="AuthenticationVCLoginButton"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="2qO-rj-st6"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="8kK-yb-srh"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<inset key="contentEdgeInsets" minX="30" minY="0.0" maxX="30" maxY="0.0"/>
<state key="normal" title="Start Verification">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="startVerificationButtonAction:" destination="V8j-Lb-PgC" eventType="touchUpInside" id="3Nd-Km-ucJ"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="To be secure, do this in person or use another way to communicate." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vli-IZ-UjP">
<rect key="frame" x="20" y="130" width="335" height="29"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="3sZ-h5-05D" firstAttribute="centerX" secondItem="dQD-Vs-7h6" secondAttribute="centerX" id="2OM-Zh-gnO"/>
<constraint firstAttribute="trailing" secondItem="0QC-yi-0hN" secondAttribute="trailing" constant="20" id="70Z-qZ-AG4"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="3sZ-h5-05D" secondAttribute="trailing" constant="20" id="ABg-Iy-IdN"/>
<constraint firstItem="Vli-IZ-UjP" firstAttribute="top" secondItem="3sZ-h5-05D" secondAttribute="bottom" constant="20" id="BIE-UM-I0Z"/>
<constraint firstItem="3sZ-h5-05D" firstAttribute="top" secondItem="b2K-LP-jqv" secondAttribute="bottom" constant="20" id="HCf-2F-jBr"/>
<constraint firstItem="Vli-IZ-UjP" firstAttribute="leading" secondItem="dQD-Vs-7h6" secondAttribute="leading" constant="20" id="Nz0-mv-fF5"/>
<constraint firstItem="3sZ-h5-05D" firstAttribute="width" secondItem="dQD-Vs-7h6" secondAttribute="width" priority="250" id="RuV-mq-5JZ"/>
<constraint firstAttribute="trailing" secondItem="Vli-IZ-UjP" secondAttribute="trailing" constant="20" id="XXe-5j-SxE"/>
<constraint firstAttribute="bottom" secondItem="Vli-IZ-UjP" secondAttribute="bottom" constant="20" id="Yfh-kG-UKJ"/>
<constraint firstItem="b2K-LP-jqv" firstAttribute="leading" secondItem="dQD-Vs-7h6" secondAttribute="leading" constant="20" id="dgE-c5-D4E"/>
<constraint firstItem="0QC-yi-0hN" firstAttribute="leading" secondItem="dQD-Vs-7h6" secondAttribute="leading" constant="20" id="g4e-WC-LQ7"/>
<constraint firstItem="0QC-yi-0hN" firstAttribute="centerY" secondItem="3sZ-h5-05D" secondAttribute="centerY" id="hqE-9r-yhx"/>
<constraint firstAttribute="height" priority="250" id="j1W-Y7-dYc"/>
<constraint firstItem="3sZ-h5-05D" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="dQD-Vs-7h6" secondAttribute="leading" constant="20" id="oP3-BI-kYI"/>
<constraint firstAttribute="trailing" secondItem="b2K-LP-jqv" secondAttribute="trailing" constant="20" id="uJ5-fk-7Xw"/>
<constraint firstItem="b2K-LP-jqv" firstAttribute="top" secondItem="dQD-Vs-7h6" secondAttribute="top" constant="10" id="vlb-Yg-HWD"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="dQD-Vs-7h6" firstAttribute="centerY" secondItem="bFg-jh-JZB" secondAttribute="centerY" id="GmI-7r-ejI"/>
<constraint firstItem="dQD-Vs-7h6" firstAttribute="trailing" secondItem="bFg-jh-JZB" secondAttribute="trailing" id="XX2-fS-8cb"/>
<constraint firstItem="dQD-Vs-7h6" firstAttribute="leading" secondItem="bFg-jh-JZB" secondAttribute="leading" id="yKt-js-MnW"/>
</constraints>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
</view>
<connections>
<outlet property="additionalInformationLabel" destination="Vli-IZ-UjP" id="4UB-LL-Om3"/>
<outlet property="informationLabel" destination="b2K-LP-jqv" id="O4J-7t-Dn6"/>
<outlet property="startVerificationButton" destination="3sZ-h5-05D" id="vNi-m9-x7g"/>
<outlet property="verificationWaitingLabel" destination="0QC-yi-0hN" id="Qoz-cV-Su7"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-3198" y="-647"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,230 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
final class UserVerificationStartViewController: UIViewController {
// MARK: - Constants
private enum Constants {
static let verifyButtonCornerRadius: CGFloat = 8.0
static let informationTextDefaultFont = UIFont.systemFont(ofSize: 15.0)
static let informationTextBoldFont = UIFont.systemFont(ofSize: 15.0, weight: .medium)
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var informationLabel: UILabel!
@IBOutlet private weak var startVerificationButton: UIButton!
@IBOutlet private weak var verificationWaitingLabel: UILabel!
@IBOutlet private weak var additionalInformationLabel: UILabel!
// MARK: Private
private var viewModel: UserVerificationStartViewModelType!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
// MARK: - Setup
class func instantiate(with viewModel: UserVerificationStartViewModelType) -> UserVerificationStartViewController {
let viewController = StoryboardScene.UserVerificationStartViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = VectorL10n.keyVerificationUserTitle
self.setupViews()
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.startVerificationButton.layer.cornerRadius = Constants.verifyButtonCornerRadius
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.headerBackgroundColor
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
self.informationLabel.textColor = theme.textPrimaryColor
self.startVerificationButton.vc_setBackgroundColor(theme.tintColor, for: .normal)
self.verificationWaitingLabel.textColor = theme.textSecondaryColor
self.additionalInformationLabel.textColor = theme.textSecondaryColor
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupViews() {
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
self?.cancelButtonAction()
}
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
self.startVerificationButton.layer.masksToBounds = true
self.startVerificationButton.setTitle(VectorL10n.userVerificationStartVerifyAction, for: .normal)
self.additionalInformationLabel.text = VectorL10n.userVerificationStartAdditionalInformation
}
private func render(viewState: UserVerificationStartViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded(let viewData):
self.renderLoaded(viewData: viewData)
case .error(let error):
self.render(error: error)
case .verificationPending:
self.renderVerificationPending()
case .cancelled(let reason):
self.renderCancelled(reason: reason)
case .cancelledByMe(let reason):
self.renderCancelledByMe(reason: reason)
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(viewData: UserVerificationStartViewData) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.informationLabel.attributedText = self.buildInformationAttributedText(with: viewData.userId)
self.verificationWaitingLabel.text = self.buildVerificationWaitingText(with: viewData)
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
private func renderVerificationPending() {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.startVerificationButton.isHidden = true
self.verificationWaitingLabel.isHidden = false
}
private func renderCancelled(reason: MXTransactionCancelCode) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, title: "", message: VectorL10n.deviceVerificationCancelled, animated: true) {
self.viewModel.process(viewAction: .cancel)
}
}
private func renderCancelledByMe(reason: MXTransactionCancelCode) {
if reason.value != MXTransactionCancelCode.user().value {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, title: "", message: VectorL10n.deviceVerificationCancelledByMe(reason.humanReadable), animated: true) {
self.viewModel.process(viewAction: .cancel)
}
} else {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
}
}
private func buildInformationAttributedText(with userId: String) -> NSAttributedString {
let informationAttributedText: NSMutableAttributedString = NSMutableAttributedString()
let informationTextDefaultAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: self.theme.textPrimaryColor,
.font: Constants.informationTextDefaultFont]
let informationTextBoldAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: self.theme.textPrimaryColor,
.font: Constants.informationTextBoldFont]
let informationAttributedStringPart1 = NSAttributedString(string: VectorL10n.userVerificationStartInformationPart1, attributes: informationTextDefaultAttributes)
let informationAttributedStringPart2 = NSAttributedString(string: userId, attributes: informationTextBoldAttributes)
let informationAttributedStringPart3 = NSAttributedString(string: VectorL10n.userVerificationStartInformationPart2, attributes: informationTextDefaultAttributes)
informationAttributedText.append(informationAttributedStringPart1)
informationAttributedText.append(informationAttributedStringPart2)
informationAttributedText.append(informationAttributedStringPart3)
return informationAttributedText
}
private func buildVerificationWaitingText(with viewData: UserVerificationStartViewData) -> String {
let userName = viewData.userDisplayName ?? viewData.userId
return VectorL10n.userVerificationStartWaitingPartner(userName)
}
// MARK: - Actions
@IBAction private func startVerificationButtonAction(_ sender: Any) {
self.viewModel.process(viewAction: .startVerification)
}
private func cancelButtonAction() {
self.viewModel.process(viewAction: .cancel)
}
}
// MARK: - UserVerificationStartViewModelViewDelegate
extension UserVerificationStartViewController: UserVerificationStartViewModelViewDelegate {
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, didUpdateViewState viewSate: UserVerificationStartViewState) {
self.render(viewState: viewSate)
}
}
@@ -0,0 +1,162 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
enum UserVerificationStartViewModelError: Error {
case keyVerificationRequestExpired
}
struct UserVerificationStartViewData {
let userId: String
let userDisplayName: String?
let userAvatarURL: String?
}
final class UserVerificationStartViewModel: UserVerificationStartViewModelType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomMember: MXRoomMember
private let verificationManager: MXKeyVerificationManager
private let keyVerificationService: KeyVerificationService
private var keyVerificationRequest: MXKeyVerificationRequest?
private var viewData: UserVerificationStartViewData {
return UserVerificationStartViewData(userId: self.roomMember.userId, userDisplayName: self.roomMember.displayname, userAvatarURL: self.roomMember.avatarUrl)
}
// MARK: Public
weak var viewDelegate: UserVerificationStartViewModelViewDelegate?
weak var coordinatorDelegate: UserVerificationStartViewModelCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, roomMember: MXRoomMember) {
self.session = session
self.verificationManager = session.crypto.keyVerificationManager
self.roomMember = roomMember
self.keyVerificationService = KeyVerificationService()
}
deinit {
}
// MARK: - Public
func process(viewAction: UserVerificationStartViewAction) {
switch viewAction {
case .loadData:
self.loadData()
case .startVerification:
self.startVerification()
case .cancel:
self.cancelKeyVerificationRequest()
self.coordinatorDelegate?.userVerificationStartViewModelDidCancel(self)
}
}
// MARK: - Private
private func loadData() {
self.update(viewState: .loaded(self.viewData))
}
private func startVerification() {
self.update(viewState: .verificationPending)
self.verificationManager.requestVerificationByDM(withUserId: self.roomMember.userId,
roomId: nil,
fallbackText: "",
methods: self.keyVerificationService.supportedKeyVerificationMethods(),
success: { [weak self] (keyVerificationRequest) in
guard let self = self else {
return
}
self.keyVerificationRequest = keyVerificationRequest
self.update(viewState: .loaded(self.viewData))
self.registerKeyVerificationRequestDidChangeNotification(for: keyVerificationRequest)
}, failure: { [weak self] error in
self?.update(viewState: .error(error))
})
}
private func update(viewState: UserVerificationStartViewState) {
self.viewDelegate?.userVerificationStartViewModel(self, didUpdateViewState: viewState)
}
private func cancelKeyVerificationRequest() {
guard let keyVerificationRequest = self.keyVerificationRequest else {
return
}
keyVerificationRequest.cancel(with: MXTransactionCancelCode.user(), success: nil, failure: nil)
}
// MARK: - MXKeyVerificationRequestDidChange
private func registerKeyVerificationRequestDidChangeNotification(for keyVerificationRequest: MXKeyVerificationRequest) {
NotificationCenter.default.addObserver(self, selector: #selector(keyVerificationRequestDidChange(notification:)), name: .MXKeyVerificationRequestDidChange, object: keyVerificationRequest)
}
private func unregisterKeyVerificationRequestDidChangeNotification() {
NotificationCenter.default.removeObserver(self, name: .MXKeyVerificationRequestDidChange, object: nil)
}
@objc private func keyVerificationRequestDidChange(notification: Notification) {
guard let keyVerificationRequest = notification.object as? MXKeyVerificationByDMRequest else {
return
}
guard let currentKeyVerificationRequest = self.keyVerificationRequest, keyVerificationRequest.requestId == currentKeyVerificationRequest.requestId else {
return
}
switch keyVerificationRequest.state {
case MXKeyVerificationRequestStateAccepted:
self.unregisterKeyVerificationRequestDidChangeNotification()
self.coordinatorDelegate?.userVerificationStartViewModel(self, otherDidAcceptRequest: currentKeyVerificationRequest)
case MXKeyVerificationRequestStateReady:
self.unregisterKeyVerificationRequestDidChangeNotification()
self.coordinatorDelegate?.userVerificationStartViewModel(self, otherDidAcceptRequest: currentKeyVerificationRequest)
case MXKeyVerificationRequestStateCancelled:
guard let reason = keyVerificationRequest.reasonCancelCode else {
return
}
self.unregisterKeyVerificationRequestDidChangeNotification()
self.update(viewState: .cancelled(reason))
case MXKeyVerificationRequestStateCancelledByMe:
guard let reason = keyVerificationRequest.reasonCancelCode else {
return
}
self.unregisterKeyVerificationRequestDidChangeNotification()
self.update(viewState: .cancelledByMe(reason))
case MXKeyVerificationRequestStateExpired:
self.unregisterKeyVerificationRequestDidChangeNotification()
self.update(viewState: .error(UserVerificationStartViewModelError.keyVerificationRequestExpired))
default:
break
}
}
}
@@ -0,0 +1,43 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationStartViewModelViewDelegate: class {
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, didUpdateViewState viewSate: UserVerificationStartViewState)
}
protocol UserVerificationStartViewModelCoordinatorDelegate: class {
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, otherDidAcceptRequest request: MXKeyVerificationRequest)
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, didCompleteWithIncomingTransaction transaction: MXSASTransaction)
func userVerificationStartViewModel(_ viewModel: UserVerificationStartViewModelType, didTransactionCancelled transaction: MXSASTransaction)
func userVerificationStartViewModelDidCancel(_ viewModel: UserVerificationStartViewModelType)
}
/// Protocol describing the view model used by `UserVerificationStartViewController`
protocol UserVerificationStartViewModelType {
var viewDelegate: UserVerificationStartViewModelViewDelegate? { get set }
var coordinatorDelegate: UserVerificationStartViewModelCoordinatorDelegate? { get set }
func process(viewAction: UserVerificationStartViewAction)
}
@@ -0,0 +1,29 @@
// File created from ScreenTemplate
// $ createScreen.sh Start UserVerificationStart
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
/// UserVerificationStartViewController view state
enum UserVerificationStartViewState {
case loading
case loaded(UserVerificationStartViewData)
case verificationPending
case cancelled(MXTransactionCancelCode)
case cancelledByMe(MXTransactionCancelCode)
case error(Error)
}
@@ -0,0 +1,183 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh UserVerification UserVerification
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
@objcMembers
final class UserVerificationCoordinator: NSObject, UserVerificationCoordinatorType {
// MARK: - Properties
// MARK: Private
private let presenter: Presentable
private let navigationRouter: NavigationRouterType
private let session: MXSession
private let userId: String
private let userDisplayName: String?
private var deviceId: String?
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: UserVerificationCoordinatorDelegate?
// MARK: - Setup
init(presenter: Presentable, session: MXSession, userId: String, userDisplayName: String?) {
self.presenter = presenter
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
self.session = session
self.userId = userId
self.userDisplayName = userDisplayName
}
convenience init(presenter: Presentable, session: MXSession, userId: String, userDisplayName: String?, deviceId: String) {
self.init(presenter: presenter, session: session, userId: userId, userDisplayName: userDisplayName)
self.deviceId = deviceId
}
// MARK: - Public methods
func start() {
// Do not start again if existing coordinators are presented
guard self.childCoordinators.isEmpty else {
return
}
guard self.session.crypto.crossSigning.isBootstrapped else {
self.presentBootstrapNotSetup()
return
}
let rootCoordinator: Coordinator & Presentable
if let deviceId = self.deviceId {
rootCoordinator = self.createSessionStatusCoordinator(with: deviceId, for: self.userId, userDisplayName: self.userDisplayName)
} else {
rootCoordinator = self.createUserVerificationSessionsStatusCoordinator()
}
rootCoordinator.start()
self.add(childCoordinator: rootCoordinator)
self.navigationRouter.setRootModule(rootCoordinator, hideNavigationBar: true, animated: false, popCompletion: {
self.remove(childCoordinator: rootCoordinator)
})
let rootViewController = self.navigationRouter.toPresentable()
rootViewController.modalPresentationStyle = .formSheet
self.presenter.toPresentable().present(rootViewController, animated: true, completion: nil)
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
// MARK: - Private methods
private func createUserVerificationSessionsStatusCoordinator() -> UserVerificationSessionsStatusCoordinator {
let coordinator = UserVerificationSessionsStatusCoordinator(session: self.session, userId: self.userId)
coordinator.delegate = self
return coordinator
}
private func createSessionStatusCoordinator(with deviceId: String, for userId: String, userDisplayName: String?) -> UserVerificationSessionStatusCoordinator {
let coordinator = UserVerificationSessionStatusCoordinator(session: self.session, userId: userId, userDisplayName: userDisplayName, deviceId: deviceId)
coordinator.delegate = self
return coordinator
}
private func presentSessionStatus(with deviceId: String, for userId: String, userDisplayName: String?) {
let coordinator = self.createSessionStatusCoordinator(with: deviceId, for: userId, userDisplayName: userDisplayName)
coordinator.start()
self.navigationRouter.push(coordinator, animated: true) {
self.remove(childCoordinator: coordinator)
}
}
private func presentDeviceVerification(for deviceId: String) {
let keyVerificationCoordinator = KeyVerificationCoordinator(navigationRouter: self.navigationRouter, session: self.session, userId: self.userId, otherDeviceId: deviceId)
keyVerificationCoordinator.delegate = self
keyVerificationCoordinator.start()
self.add(childCoordinator: keyVerificationCoordinator)
self.navigationRouter.push(keyVerificationCoordinator, animated: true, popCompletion: {
self.remove(childCoordinator: keyVerificationCoordinator)
})
}
private func presentBootstrapNotSetup() {
let alert = UIAlertController(title: VectorL10n.keyVerificationBootstrapNotSetupTitle,
message: VectorL10n.keyVerificationBootstrapNotSetupMessage,
preferredStyle: .alert)
let cancelAction = UIAlertAction(title: Bundle.mxk_localizedString(forKey: "ok"), style: .cancel, handler: { _ in
})
alert.addAction(cancelAction)
self.presenter.toPresentable().present(alert, animated: true, completion: nil)
}
}
// MARK: - UserVerificationSessionsStatusCoordinatorDelegate
extension UserVerificationCoordinator: UserVerificationSessionsStatusCoordinatorDelegate {
func userVerificationSessionsStatusCoordinatorDidClose(_ coordinator: UserVerificationSessionsStatusCoordinatorType) {
self.presenter.toPresentable().dismiss(animated: true) {
self.remove(childCoordinator: coordinator)
}
}
func userVerificationSessionsStatusCoordinator(_ coordinator: UserVerificationSessionsStatusCoordinatorType, didSelectDeviceWithId deviceId: String, for userId: String) {
self.presentSessionStatus(with: deviceId, for: userId, userDisplayName: self.userDisplayName)
}
}
// MARK: - UserVerificationSessionStatusCoordinatorDelegate
extension UserVerificationCoordinator: UserVerificationSessionStatusCoordinatorDelegate {
func userVerificationSessionStatusCoordinator(_ coordinator: UserVerificationSessionStatusCoordinatorType, wantsToManuallyVerifyDeviceWithId deviceId: String, for userId: String) {
self.presentDeviceVerification(for: deviceId)
}
func userVerificationSessionStatusCoordinatorDidClose(_ coordinator: UserVerificationSessionStatusCoordinatorType) {
self.presenter.toPresentable().dismiss(animated: true) {
self.remove(childCoordinator: coordinator)
}
}
}
// MARK: - UserVerificationCoordinatorDelegate
extension UserVerificationCoordinator: KeyVerificationCoordinatorDelegate {
func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) {
self.presenter.toPresentable().dismiss(animated: true) {
self.remove(childCoordinator: coordinator)
}
}
}
@@ -0,0 +1,84 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh UserVerification UserVerification
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
@objc protocol UserVerificationCoordinatorBridgePresenterDelegate {
func userVerificationCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: UserVerificationCoordinatorBridgePresenter)
}
/// UserVerificationCoordinatorBridgePresenter enables to start UserVerificationCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
@objcMembers
final class UserVerificationCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let presenter: Presentable
private let session: MXSession
private let userId: String
private let userDisplayName: String?
private var deviceId: String?
private var coordinator: Coordinator?
// MARK: Public
weak var delegate: UserVerificationCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(presenter: UIViewController, session: MXSession, userId: String, userDisplayName: String?) {
self.presenter = presenter
self.session = session
self.userId = userId
self.userDisplayName = userDisplayName
super.init()
}
init(presenter: UIViewController, session: MXSession, userId: String, userDisplayName: String?, deviceId: String) {
self.presenter = presenter
self.session = session
self.userId = userId
self.userDisplayName = userDisplayName
self.deviceId = deviceId
super.init()
}
// MARK: - Public
func start() {
self.present()
}
func present() {
let userVerificationCoordinator: UserVerificationCoordinator
if let deviceId = self.deviceId {
userVerificationCoordinator = UserVerificationCoordinator(presenter: self.presenter, session: self.session, userId: self.userId, userDisplayName: self.userDisplayName, deviceId: deviceId)
} else {
userVerificationCoordinator = UserVerificationCoordinator(presenter: self.presenter, session: self.session, userId: self.userId, userDisplayName: self.userDisplayName)
}
userVerificationCoordinator.start()
self.coordinator = userVerificationCoordinator
}
}
@@ -0,0 +1,28 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh UserVerification UserVerification
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol UserVerificationCoordinatorDelegate: class {
func userVerificationCoordinatorDidComplete(_ coordinator: UserVerificationCoordinatorType)
}
/// `UserVerificationCoordinatorType` is a protocol describing a Coordinator that handle user verification navigation flow.
protocol UserVerificationCoordinatorType: Coordinator, Presentable {
var delegate: UserVerificationCoordinatorDelegate? { get }
}