/* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018 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 "MXKDeviceView.h" #import "NSBundle+MatrixKit.h" #import "MXKConstants.h" #import "MXKSwiftHeader.h" static NSAttributedString *verticalWhitespace = nil; @interface MXKDeviceView () { /** The displayed device */ MXDevice *mxDevice; /** The matrix session. */ MXSession *mxSession; /** The current alert */ UIAlertController *currentAlert; /** Current request in progress. */ MXHTTPOperation *mxCurrentOperation; } @end @implementation MXKDeviceView + (UINib *)nib { // Check whether a nib file is available NSBundle *mainBundle = [NSBundle mxk_bundleForClass:self.class]; NSString *path = [mainBundle pathForResource:NSStringFromClass([self class]) ofType:@"nib"]; if (path) { return [UINib nibWithNibName:NSStringFromClass([self class]) bundle:mainBundle]; } return [UINib nibWithNibName:NSStringFromClass([MXKDeviceView class]) bundle:[NSBundle mxk_bundleForClass:[MXKDeviceView class]]]; } - (void)awakeFromNib { [super awakeFromNib]; // Add tap recognizer to discard the view on bg view tap UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onBgViewTap:)]; [tap setNumberOfTouchesRequired:1]; [tap setNumberOfTapsRequired:1]; [tap setDelegate:self]; [self.bgView addGestureRecognizer:tap]; // Localize string [_cancelButton setTitle:[MatrixKitL10n ok] forState:UIControlStateNormal]; [_cancelButton setTitle:[MatrixKitL10n ok] forState:UIControlStateHighlighted]; [_renameButton setTitle:[MatrixKitL10n rename] forState:UIControlStateNormal]; [_renameButton setTitle:[MatrixKitL10n rename] forState:UIControlStateHighlighted]; [_deleteButton setTitle:[MatrixKitL10n delete] forState:UIControlStateNormal]; [_deleteButton setTitle:[MatrixKitL10n delete] forState:UIControlStateHighlighted]; } - (void)layoutSubviews { [super layoutSubviews]; // Scroll to the top the text view content self.textView.contentOffset = CGPointZero; } #pragma mark - Override MXKView -(void)customizeViewRendering { [super customizeViewRendering]; _defaultTextColor = [UIColor blackColor]; // Add shadow on added view _containerView.layer.cornerRadius = 5; _containerView.layer.shadowOffset = CGSizeMake(0, 1); _containerView.layer.shadowOpacity = 0.5f; } #pragma mark - - (void)removeFromSuperviewDidUpdate:(BOOL)isUpdated { if (currentAlert) { [currentAlert dismissViewControllerAnimated:NO completion:nil]; currentAlert = nil; } if (mxCurrentOperation) { [mxCurrentOperation cancel]; mxCurrentOperation = nil; } if (self.delegate && [self.delegate respondsToSelector:@selector(dismissDeviceView:didUpdate:)]) { [self.delegate dismissDeviceView:self didUpdate:isUpdated]; } else { [self removeFromSuperview]; } } - (instancetype)initWithDevice:(MXDevice*)device andMatrixSession:(MXSession*)session { self = [[[self class] nib] instantiateWithOwner:nil options:nil].firstObject; if (self) { mxDevice = device; mxSession = session; [self setTranslatesAutoresizingMaskIntoConstraints: NO]; if (mxDevice) { // Device information NSMutableAttributedString *deviceInformationString = [[NSMutableAttributedString alloc] initWithString:[MatrixKitL10n deviceDetailsTitle] attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:15]}]; [deviceInformationString appendAttributedString:[MXKDeviceView verticalWhitespace]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:[MatrixKitL10n deviceDetailsName] attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:device.displayName.length ? device.displayName : @"" attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont systemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[MXKDeviceView verticalWhitespace]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:[MatrixKitL10n deviceDetailsIdentifier] attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:device.deviceId attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont systemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[MXKDeviceView verticalWhitespace]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:[MatrixKitL10n deviceDetailsLastSeen] attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; NSDate *lastSeenDate = [NSDate dateWithTimeIntervalSince1970:device.lastSeenTs/1000]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]]]; [dateFormatter setDateStyle:NSDateFormatterShortStyle]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; NSString *lastSeen = [MatrixKitL10n deviceDetailsLastSeenFormat:device.lastSeenIp :[dateFormatter stringFromDate:lastSeenDate]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:lastSeen attributes:@{NSForegroundColorAttributeName : _defaultTextColor, NSFontAttributeName: [UIFont systemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[MXKDeviceView verticalWhitespace]]; self.textView.attributedText = deviceInformationString; } else { _textView.text = nil; } // Hide potential activity indicator [_activityIndicator stopAnimating]; } return self; } - (void)dealloc { mxDevice = nil; mxSession = nil; } + (NSAttributedString *)verticalWhitespace { if (verticalWhitespace == nil) { verticalWhitespace = [[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:4]}]; } return verticalWhitespace; } #pragma mark - Actions - (IBAction)onBgViewTap:(UITapGestureRecognizer*)sender { [self removeFromSuperviewDidUpdate:NO]; } - (IBAction)onButtonPressed:(id)sender { if (sender == _cancelButton) { [self removeFromSuperviewDidUpdate:NO]; } else if (sender == _renameButton) { [self renameDevice]; } else if (sender == _deleteButton) { [self deleteDevice]; } } #pragma mark - - (void)renameDevice { if (!self.delegate) { // Ignore MXLogDebug(@"[MXKDeviceView] Rename device failed, delegate is missing"); return; } // Prompt the user to enter a device name. [currentAlert dismissViewControllerAnimated:NO completion:nil]; __weak typeof(self) weakSelf = self; currentAlert = [UIAlertController alertControllerWithTitle:[MatrixKitL10n deviceDetailsRenamePromptTitle] message:[MatrixKitL10n deviceDetailsRenamePromptMessage] preferredStyle:UIAlertControllerStyleAlert]; [currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.secureTextEntry = NO; textField.placeholder = nil; textField.keyboardType = UIKeyboardTypeDefault; if (weakSelf) { typeof(self) self = weakSelf; textField.text = self->mxDevice.displayName; } }]; [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; self->currentAlert = nil; } }]]; [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; NSString *text = [self->currentAlert textFields].firstObject.text; self->currentAlert = nil; [self.activityIndicator startAnimating]; self->mxCurrentOperation = [self->mxSession.matrixRestClient setDeviceName:text forDeviceId:self->mxDevice.deviceId success:^{ if (weakSelf) { typeof(self) self = weakSelf; self->mxCurrentOperation = nil; [self.activityIndicator stopAnimating]; [self removeFromSuperviewDidUpdate:YES]; } } failure:^(NSError *error) { if (weakSelf) { typeof(self) self = weakSelf; // Notify MatrixKit user NSString *myUserId = self->mxSession.myUser.userId; [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil]; self->mxCurrentOperation = nil; MXLogDebug(@"[MXKDeviceView] Rename device (%@) failed", self->mxDevice.deviceId); [self.activityIndicator stopAnimating]; [self removeFromSuperviewDidUpdate:NO]; } }]; } }]]; [self.delegate deviceView:self presentAlertController:currentAlert]; } - (void)deleteDevice { if (!self.delegate) { // Ignore MXLogDebug(@"[MXKDeviceView] Delete device failed, delegate is missing"); return; } // Get an authentication session to prepare device deletion [self.activityIndicator startAnimating]; mxCurrentOperation = [mxSession.matrixRestClient getSessionToDeleteDeviceByDeviceId:mxDevice.deviceId success:^(MXAuthenticationSession *authSession) { self->mxCurrentOperation = nil; // Check whether the password based type is supported BOOL isPasswordBasedTypeSupported = NO; for (MXLoginFlow *loginFlow in authSession.flows) { if ([loginFlow.type isEqualToString:kMXLoginFlowTypePassword] || [loginFlow.stages indexOfObject:kMXLoginFlowTypePassword] != NSNotFound) { isPasswordBasedTypeSupported = YES; break; } } if (isPasswordBasedTypeSupported && authSession.session) { // Prompt for a password [self->currentAlert dismissViewControllerAnimated:NO completion:nil]; __weak typeof(self) weakSelf = self; // Prompt the user before deleting the device. self->currentAlert = [UIAlertController alertControllerWithTitle:[MatrixKitL10n deviceDetailsDeletePromptTitle] message:[MatrixKitL10n deviceDetailsDeletePromptMessage] preferredStyle:UIAlertControllerStyleAlert]; [self->currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.secureTextEntry = YES; textField.placeholder = nil; textField.keyboardType = UIKeyboardTypeDefault; }]; [self->currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; self->currentAlert = nil; [self.activityIndicator stopAnimating]; } }]]; [self->currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n submit] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; UITextField *textField = [self->currentAlert textFields].firstObject; self->currentAlert = nil; NSString *userId = self->mxSession.myUser.userId; NSDictionary *authParams; // Sanity check if (userId) { authParams = @{@"session":authSession.session, @"user": userId, @"password": textField.text, @"type": kMXLoginFlowTypePassword}; } self->mxCurrentOperation = [self->mxSession.matrixRestClient deleteDeviceByDeviceId:self->mxDevice.deviceId authParams:authParams success:^{ if (weakSelf) { typeof(self) self = weakSelf; self->mxCurrentOperation = nil; [self.activityIndicator stopAnimating]; [self removeFromSuperviewDidUpdate:YES]; } } failure:^(NSError *error) { if (weakSelf) { typeof(self) self = weakSelf; // Notify MatrixKit user NSString *myUserId = self->mxSession.myUser.userId; [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil]; self->mxCurrentOperation = nil; MXLogDebug(@"[MXKDeviceView] Delete device (%@) failed", self->mxDevice.deviceId); [self.activityIndicator stopAnimating]; [self removeFromSuperviewDidUpdate:NO]; } }]; } }]]; [self.delegate deviceView:self presentAlertController:self->currentAlert]; } else { MXLogDebug(@"[MXKDeviceView] Delete device (%@) failed, auth session flow type is not supported", self->mxDevice.deviceId); [self.activityIndicator stopAnimating]; } } failure:^(NSError *error) { self->mxCurrentOperation = nil; MXLogDebug(@"[MXKDeviceView] Delete device (%@) failed, unable to get auth session", self->mxDevice.deviceId); [self.activityIndicator stopAnimating]; // Notify MatrixKit user NSString *myUserId = self->mxSession.myUser.userId; [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil]; }]; } @end