Commit e79b903d authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Andreas Traczyk

Account: link new device

Add functionality to fetch user password and display PIN that could
be used to link a new device

Change-Id: Ie985b797af64ebe0de1bea9ac64292e427f5302f
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent 44fb2400
......@@ -86,6 +86,10 @@
0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */; };
0EB1A5CF1F8EBE03009923E2 /* DeviceCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0EB1A5CE1F8EBE03009923E2 /* DeviceCell.xib */; };
0EB1A5D11F8EBE23009923E2 /* DeviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB1A5D01F8EBE23009923E2 /* DeviceCell.swift */; };
0ED2B6FA1F96A075001572F0 /* LinkNewDeviceViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */; };
0ED2B6FC1F96A158001572F0 /* LinkNewDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */; };
0ED2B6FE1F96A16C001572F0 /* LinkNewDeviceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED2B6FD1F96A16C001572F0 /* LinkNewDeviceViewModel.swift */; };
0EDCC8601F98150500B121D7 /* UIView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDCC85F1F98150500B121D7 /* UIView+Rx.swift */; };
0EDE34C71F868E1200FFA15C /* EditProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE34C61F868E1200FFA15C /* EditProfileViewController.swift */; };
0EDE34C91F8691BB00FFA15C /* EditProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE34C81F8691BB00FFA15C /* EditProfileViewModel.swift */; };
0EE1B54E1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1B54D1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift */; };
......@@ -312,6 +316,10 @@
0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellGenerated.xib; sourceTree = "<group>"; };
0EB1A5CE1F8EBE03009923E2 /* DeviceCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DeviceCell.xib; sourceTree = "<group>"; };
0EB1A5D01F8EBE23009923E2 /* DeviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCell.swift; sourceTree = "<group>"; };
0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LinkNewDeviceViewController.storyboard; sourceTree = "<group>"; };
0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceViewController.swift; sourceTree = "<group>"; };
0ED2B6FD1F96A16C001572F0 /* LinkNewDeviceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceViewModel.swift; sourceTree = "<group>"; };
0EDCC85F1F98150500B121D7 /* UIView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Rx.swift"; sourceTree = "<group>"; };
0EDE34C61F868E1200FFA15C /* EditProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditProfileViewController.swift; sourceTree = "<group>"; };
0EDE34C81F8691BB00FFA15C /* EditProfileViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditProfileViewModel.swift; sourceTree = "<group>"; };
0EE1B54D1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CNContactVCardSerialization+Helpers.swift"; sourceTree = "<group>"; };
......@@ -646,6 +654,7 @@
0586C94A1F684DF600613517 /* UIImage+Helpers.swift */,
0EE1B54D1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift */,
621231F81F880EDF009B86F0 /* UILabel+Ring.swift */,
0EDCC85F1F98150500B121D7 /* UIView+Rx.swift */,
);
path = Extensions;
sourceTree = "<group>";
......@@ -805,6 +814,16 @@
name = DeviceCell;
sourceTree = "<group>";
};
0ED2B6F81F96A048001572F0 /* LinknewDevice */ = {
isa = PBXGroup;
children = (
0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */,
0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */,
0ED2B6FD1F96A16C001572F0 /* LinkNewDeviceViewModel.swift */,
);
name = LinknewDevice;
sourceTree = "<group>";
};
0EDE34C51F868D2D00FFA15C /* Shared */ = {
isa = PBXGroup;
children = (
......@@ -913,6 +932,7 @@
1A2D18A81F290FBF00B2C785 /* Me */ = {
isa = PBXGroup;
children = (
0ED2B6F81F96A048001572F0 /* LinknewDevice */,
1A2D18D91F2918F300B2C785 /* Me */,
1A2D18AF1F29158700B2C785 /* Detail */,
1A2D18AB1F29149D00B2C785 /* MeCoordinator.swift */,
......@@ -1204,6 +1224,7 @@
1A2D18E61F29197100B2C785 /* MessageAccessoryView.xib in Resources */,
1A2D18F81F292D7200B2C785 /* MessageCellSent.xib in Resources */,
1A2D18F61F292D7200B2C785 /* MessageCellReceived.xib in Resources */,
0ED2B6FA1F96A075001572F0 /* LinkNewDeviceViewController.storyboard in Resources */,
1A2D18EF1F291A0100B2C785 /* MeDetailViewController.storyboard in Resources */,
1A2D18B11F2915B600B2C785 /* SmartlistViewController.storyboard in Resources */,
0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */,
......@@ -1347,6 +1368,7 @@
0EE1B54E1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift in Sources */,
56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */,
1A3CA32D1F13DA7200283748 /* Chameleon+Ring.swift in Sources */,
0ED2B6FC1F96A158001572F0 /* LinkNewDeviceViewController.swift in Sources */,
1ABE07E21F0D924700D36361 /* Strings.swift in Sources */,
621231FB1F8D6FEE009B86F0 /* MessageCell.swift in Sources */,
56AC650E1E85694D00EA1AA9 /* DesignableTextField.swift in Sources */,
......@@ -1362,6 +1384,7 @@
1A2D18AA1F29131900B2C785 /* ConversationsCoordinator.swift in Sources */,
043999F71D1C2D9D00E99CD9 /* AppDelegate.swift in Sources */,
1A2041861F1EA19600C08435 /* CreateAccountViewController.swift in Sources */,
0EDCC8601F98150500B121D7 /* UIView+Rx.swift in Sources */,
1A2D18C21F29180700B2C785 /* AccountCredentialsModel.swift in Sources */,
1A2D18FF1F29352D00B2C785 /* MeViewModel.swift in Sources */,
62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */,
......@@ -1375,6 +1398,7 @@
1A5DC0281F3564AA0075E8EF /* MessageModel.swift in Sources */,
56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */,
1A2041911F1FD46300C08435 /* DesignableView.swift in Sources */,
0ED2B6FE1F96A16C001572F0 /* LinkNewDeviceViewModel.swift in Sources */,
1A3D28A91F0EBF0200B524EE /* UIView+Ring.swift in Sources */,
1A2041881F1EA1EA00C08435 /* CreateAccountViewModel.swift in Sources */,
62E55B6D1F758E6F00D3FEF4 /* String+Helpers.swift in Sources */,
......
......@@ -79,6 +79,11 @@ enum StoryboardScene {
static let initialScene = InitialSceneType<Ring.LinkDeviceViewController>(storyboard: LinkDeviceViewController.self)
}
enum LinkNewDeviceViewController: StoryboardType {
static let storyboardName = "LinkNewDeviceViewController"
static let initialScene = InitialSceneType<Ring.LinkNewDeviceViewController>(storyboard: LinkNewDeviceViewController.self)
}
enum MeDetailViewController: StoryboardType {
static let storyboardName = "MeDetailViewController"
......
......@@ -82,6 +82,21 @@ enum L10n {
static let ok = L10n.tr("Localizable", "global.ok")
}
enum Linkdevice {
/// An error occured during the export
static let defaultError = L10n.tr("Localizable", "linkDevice.defaultError")
/// To complete the process, you need to open Ring on the new device and choose the option "Link this device to an account." Your pin is valid for 10 minutes
static let explanationMessage = L10n.tr("Localizable", "linkDevice.explanationMessage")
/// Verifying
static let hudMessage = L10n.tr("Localizable", "linkDevice.hudMessage")
/// A network error occured during the export
static let networkError = L10n.tr("Localizable", "linkDevice.networkError")
/// The password you entered does not unlock this account
static let passwordError = L10n.tr("Localizable", "linkDevice.passwordError")
/// Link new device
static let title = L10n.tr("Localizable", "linkDevice.title")
}
enum Smartlist {
/// Conversations
static let conversations = L10n.tr("Localizable", "smartlist.conversations")
......
......@@ -144,4 +144,11 @@ extension UIView {
return UIColor.clear
}
public func convertViewToImage() -> UIImage? {
UIGraphicsBeginImageContext(self.bounds.size)
self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import UIKit
import RxSwift
import RxCocoa
extension Reactive where Base : UIView {
//show view with animation and hide without
public var isVisible: AnyObserver<Bool> {
return UIBindingObserver(UIElement: self.base) { view, hidden in
if hidden == true {
view.isHidden = true
view.alpha = 0
} else {
UIView.animate(withDuration: 0.3, delay: 0.5, options: .curveEaseOut,
animations: {view.alpha = 1},
completion: { _ in view.isHidden = false
})
}
}.asObserver()
}
}
This diff is collapsed.
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import Reusable
import RxSwift
import PKHUD
class LinkNewDeviceViewController: UIViewController, StoryboardBased, ViewModelBased {
@IBOutlet weak var titleLable: UILabel!
@IBOutlet weak var passwordField: UITextField!
@IBOutlet weak var okButton: UIButton!
@IBOutlet weak var cancelButton: UIButton!
@IBOutlet weak var pinLabel: UILabel!
@IBOutlet weak var explanationMessage: UILabel!
@IBOutlet weak var errorMessage: UILabel!
@IBOutlet weak var background: UIImageView!
@IBOutlet weak var containerView: UIView!
var viewModel: LinkNewDeviceViewModel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
self.background.image = self.view.convertViewToImage()
self.applyL10n()
// initial state
self.viewModel.isInitialState
.bind(to: self.titleLable.rx.isHidden)
.addDisposableTo(self.disposeBag)
self.viewModel.isInitialState.bind(to: self.passwordField.rx.isHidden)
.addDisposableTo(self.disposeBag)
self.viewModel.isInitialState.bind(to: self.cancelButton.rx.isHidden)
.addDisposableTo(self.disposeBag)
// error state
self.viewModel.isErrorState.bind(to: self.errorMessage.rx.isVisible)
.addDisposableTo(self.disposeBag)
// success state
self.viewModel.isSuccessState
.bind(to: self.explanationMessage.rx.isVisible)
.addDisposableTo(self.disposeBag)
self.viewModel.isSuccessState
.bind(to: self.pinLabel.rx.isVisible)
.addDisposableTo(self.disposeBag)
passwordField.rx.text
.map({!$0!.isEmpty})
.shareReplay(1)
.bind(to: okButton.rx.isEnabled)
.addDisposableTo(self.disposeBag)
self.viewModel.observableState
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] (state) in
switch state {
case .generatingPin:
self?.showProgress()
case .success(let pin):
self?.pinLabel.text = pin
self?.hideHud()
case .error(let pinError):
self?.errorMessage.text = pinError.description
self?.hideHud()
default:
break
}
}).addDisposableTo(self.disposeBag)
cancelButton.rx.tap.subscribe(onNext: { [unowned self] in
self.dismiss(animated: true, completion: nil)
}).disposed(by: disposeBag)
okButton.rx.tap.subscribe(onNext: { [unowned self] in
if !self.passwordField.isHidden {
self.viewModel.linkDevice(with: self.passwordField.text)
self.passwordField.text = ""
} else if !self.errorMessage.isHidden {
self.viewModel.refresh()
} else {
self.dismiss(animated: true, completion: nil)
}
}).disposed(by: disposeBag)
super.viewDidLoad()
}
private func showProgress() {
HUD.show(.labeledProgress(title: L10n.Linkdevice.hudMessage, subtitle: nil), onView: self.containerView)
}
private func hideHud() {
HUD.hide(animated: true)
}
private func applyL10n() {
self.titleLable.text = self.viewModel.linkDeviceTitleTitle
self.explanationMessage.text = self.viewModel.explanationMessage
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import RxSwift
import RxDataSources
enum ExportAccountResponse: Int {
case success = 0
case wrongPassword = 1
case networkProblem = 2
}
enum PinError {
case passwordError
case networkError
case defaultError
var description: String {
switch self {
case .passwordError:
return L10n.Linkdevice.passwordError
case .networkError:
return L10n.Linkdevice.networkError
case .defaultError:
return L10n.Linkdevice.defaultError
}
}
}
enum GeneratingPinState {
case initial
case generatingPin
case success(pin: String)
case error(error: PinError)
var rawValue: String {
switch self {
case .initial:
return "INITIAL"
case .generatingPin:
return "GENERATING_PIN"
case .success:
return "SUCCESS"
case .error:
return "ERROR"
}
}
func isStateOfType(type: String) -> Bool {
return self.rawValue == type
}
}
class LinkNewDeviceViewModel: ViewModel, Stateable {
// MARK: - Rx Stateable
private let stateSubject = PublishSubject <State>()
lazy var state: Observable<State> = {
return self.stateSubject.asObservable()
}()
private let generatingState = Variable(GeneratingPinState.initial)
lazy var observableState: Observable <GeneratingPinState> = {
return self.generatingState.asObservable()
}()
lazy var isInitialState: Observable<Bool> = {
return self.observableState.map { state in
return !state.isStateOfType(type: "INITIAL")
}
}().share()
lazy var isSuccessState: Observable<Bool> = {
return self.observableState.map { state in
return !state.isStateOfType(type: "SUCCESS")
}
}().share()
lazy var isErrorState: Observable<Bool> = {
return self.observableState.map { state in
return !state.isStateOfType(type: "ERROR")
}
}().share()
lazy var isGeneratedPinState: Observable<Bool> = {
return self.observableState.map { state in
return !state.isStateOfType(type: "GENERATING_PIN")
}
}().share()
let accountService: AccountsService
let disposeBag = DisposeBag()
// MARK: L10n
let linkDeviceTitleTitle = L10n.Linkdevice.title
let explanationMessage = L10n.Linkdevice.explanationMessage
required init(with injectionBag: InjectionBag) {
self.accountService = injectionBag.accountService
}
func linkDevice(with password: String?) {
self.generatingState.value = GeneratingPinState.generatingPin
guard let password = password else {
self.generatingState.value = GeneratingPinState.error(error: PinError.passwordError)
return
}
self.accountService.exportOnRing(withPassword: password).subscribe(onCompleted: {
if let account = self.accountService.currentAccount {
let accountHelper = AccountModelHelper(withAccount: account)
let uri = accountHelper.ringId
self.accountService.sharedResponseStream
.filter({ exportComplitedEvent in
return exportComplitedEvent.eventType == ServiceEventType.exportOnRingEnded
&& exportComplitedEvent.getEventInput(.uri) == uri
})
.subscribe(onNext: { [unowned self] exportComplitedEvent in
if let state: Int = exportComplitedEvent.getEventInput(.state) {
switch state {
case ExportAccountResponse.success.rawValue:
if let pin: String = exportComplitedEvent.getEventInput(.pin) {
self.generatingState.value = GeneratingPinState.success(pin: pin)
} else {
self.generatingState.value = GeneratingPinState.error(error: PinError.defaultError)
}
case ExportAccountResponse.wrongPassword.rawValue:
self.generatingState.value = GeneratingPinState.error(error: PinError.passwordError)
case ExportAccountResponse.networkProblem.rawValue:
self.generatingState.value = GeneratingPinState.error(error: PinError.networkError)
default:
self.generatingState.value = GeneratingPinState.error(error: PinError.defaultError)
}
}
})
.disposed(by: self.disposeBag)
} else {
self.generatingState.value = GeneratingPinState.error(error: PinError.defaultError)
}
})
{ error in
self.generatingState.value = GeneratingPinState.error(error: PinError.passwordError)
}.addDisposableTo(self.disposeBag)
}
func refresh() {
self.generatingState.value = GeneratingPinState.initial
}
}
......@@ -131,6 +131,6 @@ class MeViewModel: ViewModel, Stateable {
}
func linkDevice() {
self.stateSubject.onNext(MeState.linkNewDevice)
}
}
......@@ -24,8 +24,10 @@ import RxSwift
/// Represents Me navigation state
///
/// - meDetail: user want its account detail
/// -linkDevice: link new device to account
public enum MeState: State {
case meDetail
case linkNewDevice
}
/// This Coordinator drives the me/settings navigation
......@@ -51,7 +53,9 @@ class MeCoordinator: Coordinator, StateableResponsive {
switch state {
case .meDetail:
self.showMeDetail()
break
case .linkNewDevice:
self.showLinkDeviceWindow()
}
}).disposed(by: self.disposeBag)
......@@ -66,4 +70,9 @@ class MeCoordinator: Coordinator, StateableResponsive {
let meDetailViewController = MeDetailViewController.instantiate(with: self.injectionBag)
self.present(viewController: meDetailViewController, withStyle: .show, withAnimation: true)
}
private func showLinkDeviceWindow() {
let linkDeviceVC = LinkNewDeviceViewController.instantiate(with: self.injectionBag)
self.present(viewController: linkDeviceVC, withStyle: .appear, withAnimation: false)
}
}
......@@ -33,6 +33,7 @@ public enum PresentationStyle {
case show
case present
case popup
case appear
}
/// A Coordinator drives the navigation of a whole part of the application
......@@ -95,6 +96,12 @@ extension Coordinator {
break
case .show: self.rootViewController.show(viewController, sender: nil)
break
case .appear:
viewController.modalPresentationStyle = .overFullScreen
viewController.modalTransitionStyle = .crossDissolve
self.rootViewController.present(viewController,
animated: animation,
completion: nil)
}
}
}
......@@ -72,3 +72,11 @@
//Account Page
"accountPage.devicesListHeader" = "Devices";
//Link New Device
"linkDevice.title" = "Link new device";
"linkDevice.passwordError" = "The password you entered does not unlock this account";
"linkDevice.networkError" = "A network error occured during the export";
"linkDevice.defaultError" = "An error occured during the export";
"linkDevice.explanationMessage" = "To complete the process, you need to open Ring on the new device and choose the option \"Link this device to an account.\" Your pin is valid for 10 minutes";
"linkDevice.hudMessage" = "Verifying";
......@@ -410,7 +410,18 @@ class AccountsService: AccountAdapterDelegate {
func exportOnRingEndeded(forAccout account: String, state: Int, pin: String) {
log.info(pin)
let changedAccount = getAccount(fromAccountId: account)
if let changedAccount = changedAccount {
let accountHelper = AccountModelHelper(withAccount: changedAccount)
if let uri = accountHelper.ringId {
var event = ServiceEvent(withEventType: .exportOnRingEnded)
event.addEventInput(.uri, value: uri)
event.addEventInput(.state, value: state)
event.addEventInput(.pin, value: pin)
self.responseStream.onNext(event)
}
}
}
}
......@@ -31,6 +31,7 @@ enum ServiceEventType {
case presenceUpdated
case messageStateChanged
case knownDevicesChanged
case exportOnRingEnded
}
/**
......@@ -44,6 +45,7 @@ enum ServiceEventInput {
case presenceStatus
case messageStatus
case messageId
case pin
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment