mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-05 15:37:45 +02:00
Moved code under a subdirectory: Console
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/Foundation.h>
|
||||
|
||||
#import "MXRestClient.h"
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
MXC3PIDAuthStateUnknown,
|
||||
MXC3PIDAuthStateTokenRequested,
|
||||
MXC3PIDAuthStateTokenReceived,
|
||||
MXC3PIDAuthStateTokenSubmitted,
|
||||
MXC3PIDAuthStateAuthenticated
|
||||
} MXC3PIDAuthState;
|
||||
|
||||
|
||||
@interface MXC3PID : NSObject
|
||||
|
||||
/**
|
||||
The 3rd party system where the user is defined.
|
||||
*/
|
||||
@property (nonatomic, readonly) MX3PIDMedium medium;
|
||||
|
||||
/**
|
||||
The id of the user in the 3rd party system.
|
||||
*/
|
||||
@property (nonatomic, readonly) NSString *address;
|
||||
|
||||
/**
|
||||
The id of the user on Matrix.
|
||||
nil if unknown or not yet resolved.
|
||||
*/
|
||||
@property (nonatomic) NSString *userId;
|
||||
|
||||
@property (nonatomic, readonly) MXC3PIDAuthState validationState;
|
||||
|
||||
/**
|
||||
Initialise the instance with a 3PID.
|
||||
|
||||
@param medium the medium.
|
||||
@param address the id of the contact on this medium.
|
||||
@return the new instance.
|
||||
*/
|
||||
- (instancetype)initWithMedium:(NSString*)medium andAddress:(NSString*)address;
|
||||
|
||||
/**
|
||||
Start the validation process
|
||||
The identity server will send a validation token to the user's address.
|
||||
This validation token must be then send back to the identity server with [MXC3PID validateWithToken]
|
||||
in order to complete the 3PID authentication.
|
||||
|
||||
@param success A block object called when the operation succeeds.
|
||||
@param failure A block object called when the operation fails.
|
||||
*/
|
||||
- (void)requestValidationToken:(void (^)())success
|
||||
failure:(void (^)(NSError *error))failure;
|
||||
|
||||
/**
|
||||
Complete the 3rd party id validation by sending the validation token the user received.
|
||||
|
||||
@param validationToken the validation token the user received.
|
||||
@param success A block object called when the operation succeeds. It indicates if the
|
||||
validation has succeeded.
|
||||
@param failure A block object called when the operation fails.
|
||||
*/
|
||||
- (void)validateWithToken:(NSString*)validationToken
|
||||
success:(void (^)(BOOL success))success
|
||||
failure:(void (^)(NSError *error))failure;
|
||||
|
||||
/**
|
||||
Link an authenticated 3rd party id to a Matrix user id.
|
||||
|
||||
@param userId the Matrix user id to link the 3PID with.
|
||||
@param success A block object called when the operation succeeds. It provides the raw
|
||||
server response.
|
||||
@param failure A block object called when the operation fails.
|
||||
*/
|
||||
- (void)bindWithUserId:(NSString*)userId
|
||||
success:(void (^)())success
|
||||
failure:(void (^)(NSError *error))failure;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "MXC3PID.h"
|
||||
#import "MatrixSDKHandler.h"
|
||||
#import "MXTools.h"
|
||||
|
||||
@interface MXC3PID ()
|
||||
@property (nonatomic) NSString *clientSecret;
|
||||
@property (nonatomic) NSUInteger sendAttempt;
|
||||
@property (nonatomic) NSString *sid;
|
||||
@end
|
||||
|
||||
@implementation MXC3PID
|
||||
|
||||
- (instancetype)initWithMedium:(NSString *)medium andAddress:(NSString *)address
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_medium = [medium copy];
|
||||
_address = [address copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)resetValidationParameters {
|
||||
_validationState = MXC3PIDAuthStateUnknown;
|
||||
self.clientSecret = nil;
|
||||
self.sendAttempt = 1;
|
||||
self.sid = nil;
|
||||
// Removed potential linked userId
|
||||
self.userId = nil;
|
||||
}
|
||||
|
||||
- (void)requestValidationToken:(void (^)())success
|
||||
failure:(void (^)(NSError *error))failure {
|
||||
// Sanity Check
|
||||
if (_validationState != MXC3PIDAuthStateTokenRequested) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
// Reset if the current state is different than "Unknown"
|
||||
if (_validationState != MXC3PIDAuthStateUnknown) {
|
||||
[self resetValidationParameters];
|
||||
}
|
||||
|
||||
if ([self.medium isEqualToString:kMX3PIDMediumEmail]) {
|
||||
self.clientSecret = [MXTools generateSecret];
|
||||
_validationState = MXC3PIDAuthStateTokenRequested;
|
||||
[mxHandler.mxRestClient requestEmailValidation:self.address clientSecret:self.clientSecret sendAttempt:self.sendAttempt success:^(NSString *sid) {
|
||||
_validationState = MXC3PIDAuthStateTokenReceived;
|
||||
self.sid = sid;
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
} failure:^(NSError *error) {
|
||||
// Return in unknown state
|
||||
_validationState = MXC3PIDAuthStateUnknown;
|
||||
// Increment attempt counter
|
||||
self.sendAttempt++;
|
||||
if (failure) {
|
||||
failure (error);
|
||||
}
|
||||
}];
|
||||
|
||||
return;
|
||||
} else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN]) {
|
||||
// FIXME: support msisdn as soon as identity server supports it
|
||||
NSLog(@"[MXC3PID] requestValidationToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
|
||||
} else {
|
||||
NSLog(@"[MXC3PID] requestValidationToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
|
||||
}
|
||||
} else {
|
||||
NSLog(@"[MXC3PID] Failed to request validation token for 3PID: %@ (%@), state: %lu", self.address, self.medium, (unsigned long)_validationState);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)validateWithToken:(NSString*)validationToken
|
||||
success:(void (^)(BOOL success))success
|
||||
failure:(void (^)(NSError *error))failure {
|
||||
// Sanity check
|
||||
if (_validationState == MXC3PIDAuthStateTokenReceived) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
if ([self.medium isEqualToString:kMX3PIDMediumEmail]) {
|
||||
_validationState = MXC3PIDAuthStateTokenSubmitted;
|
||||
[mxHandler.mxRestClient validateEmail:self.sid validationToken:validationToken clientSecret:self.clientSecret success:^(BOOL successFlag) {
|
||||
if (successFlag) {
|
||||
// Validation is complete
|
||||
_validationState = MXC3PIDAuthStateAuthenticated;
|
||||
} else {
|
||||
// Return in previous step
|
||||
_validationState = MXC3PIDAuthStateTokenReceived;
|
||||
}
|
||||
if (success) {
|
||||
success(successFlag);
|
||||
}
|
||||
} failure:^(NSError *error) {
|
||||
// Return in previous step
|
||||
_validationState = MXC3PIDAuthStateTokenReceived;
|
||||
if (failure) {
|
||||
failure (error);
|
||||
}
|
||||
}];
|
||||
|
||||
return;
|
||||
} else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN]) {
|
||||
// FIXME: support msisdn as soon as identity server supports it
|
||||
NSLog(@"[MXC3PID] validateWithToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
|
||||
} else {
|
||||
NSLog(@"[MXC3PID] validateWithToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
|
||||
}
|
||||
} else {
|
||||
NSLog(@"[MXC3PID] Failed to valid with token 3PID: %@ (%@), state: %lu", self.address, self.medium, (unsigned long)_validationState);
|
||||
}
|
||||
|
||||
// Here the validation process failed
|
||||
if (failure) {
|
||||
failure (nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)bindWithUserId:(NSString*)userId
|
||||
success:(void (^)())success
|
||||
failure:(void (^)(NSError *error))failure {
|
||||
// Sanity check
|
||||
if (_validationState == MXC3PIDAuthStateAuthenticated) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
if ([self.medium isEqualToString:kMX3PIDMediumEmail]) {
|
||||
[mxHandler.mxRestClient bind3PID:userId sid:self.sid clientSecret:self.clientSecret success:^(NSDictionary *JSONResponse) {
|
||||
// Update linked userId in 3PID
|
||||
self.userId = userId;
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
} failure:failure];
|
||||
|
||||
return;
|
||||
} else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN]) {
|
||||
// FIXME: support msisdn as soon as identity server supports it
|
||||
NSLog(@"[MXC3PID] bindWithUserId: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
|
||||
} else {
|
||||
NSLog(@"[MXC3PID] bindWithUserId: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
|
||||
}
|
||||
} else {
|
||||
NSLog(@"[MXC3PID] Failed to bind 3PID: %@ (%@), state: %lu", self.address, self.medium, (unsigned long)_validationState);
|
||||
}
|
||||
|
||||
// Here the validation process failed
|
||||
if (failure) {
|
||||
failure (nil);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/UIKit.h>
|
||||
|
||||
// Note: UIAlertView is deprecated in iOS 8. To create and manage alerts in iOS 8 and later, instead use UIAlertController
|
||||
// with a preferredStyle of UIAlertControllerStyleAlert.
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
MXCAlertActionStyleDefault = 0,
|
||||
MXCAlertActionStyleCancel,
|
||||
MXCAlertActionStyleDestructive
|
||||
} MXCAlertActionStyle;
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
MXCAlertStyleActionSheet = 0,
|
||||
MXCAlertStyleAlert
|
||||
} MXCAlertStyle;
|
||||
|
||||
@interface MXCAlert : NSObject <UIActionSheetDelegate> {
|
||||
}
|
||||
|
||||
typedef void (^blockMXCAlert_onClick)(MXCAlert *alert);
|
||||
typedef void (^blockMXCAlert_textFieldHandler)(UITextField *textField);
|
||||
|
||||
@property(nonatomic) NSInteger cancelButtonIndex; // required to dismiss cusmtomAlert on iOS < 8 (default is -1).
|
||||
@property(nonatomic, weak) UIView *sourceView;
|
||||
|
||||
- (id)initWithTitle:(NSString *)title message:(NSString *)message style:(MXCAlertStyle)style;
|
||||
// adds a button with the title. returns the index (0 based) of where it was added.
|
||||
- (NSInteger)addActionWithTitle:(NSString *)title style:(MXCAlertActionStyle)style handler:(blockMXCAlert_onClick)handler;
|
||||
// Adds a text field to an alert (Note: You can add a text field only if the style property is set to MXCAlertStyleAlert).
|
||||
- (void)addTextFieldWithConfigurationHandler:(blockMXCAlert_textFieldHandler)configurationHandler;
|
||||
|
||||
- (void)showInViewController:(UIViewController*)viewController;
|
||||
|
||||
- (void)dismiss:(BOOL)animated;
|
||||
- (UITextField *)textFieldAtIndex:(NSInteger)textFieldIndex;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,232 @@
|
||||
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "MXCAlert.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface MXCAlert()
|
||||
{
|
||||
UIViewController* parentViewController;
|
||||
NSMutableArray *actions; // use only for iOS < 8
|
||||
}
|
||||
|
||||
@property(nonatomic, strong) id alert; // alert is kind of UIAlertController for IOS 8 and later, in other cases it's kind of UIAlertView or UIActionSheet.
|
||||
@end
|
||||
|
||||
@implementation MXCAlert
|
||||
|
||||
- (void)dealloc {
|
||||
// iOS < 8
|
||||
if ([_alert isKindOfClass:[UIActionSheet class]] || [_alert isKindOfClass:[UIAlertView class]]) {
|
||||
// Dismiss here AlertView or ActionSheet (if any) because its delegate is released
|
||||
[self dismiss:NO];
|
||||
}
|
||||
|
||||
_alert = nil;
|
||||
parentViewController = nil;
|
||||
actions = nil;
|
||||
}
|
||||
|
||||
- (id)initWithTitle:(NSString *)title message:(NSString *)message style:(MXCAlertStyle)style {
|
||||
if (self = [super init]) {
|
||||
// Check iOS version
|
||||
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8) {
|
||||
_alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:(UIAlertControllerStyle)style];
|
||||
} else {
|
||||
// Use legacy objects
|
||||
if (style == MXCAlertStyleActionSheet) {
|
||||
_alert = [[UIActionSheet alloc] initWithTitle:title delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
|
||||
} else {
|
||||
_alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:self cancelButtonTitle:nil otherButtonTitles:nil];
|
||||
}
|
||||
|
||||
self.cancelButtonIndex = -1;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (NSInteger)addActionWithTitle:(NSString *)title style:(MXCAlertActionStyle)style handler:(blockMXCAlert_onClick)handler {
|
||||
NSInteger index = 0;
|
||||
if ([_alert isKindOfClass:[UIAlertController class]]) {
|
||||
index = [(UIAlertController *)_alert actions].count;
|
||||
UIAlertAction* action = [UIAlertAction actionWithTitle:title
|
||||
style:(UIAlertActionStyle)style
|
||||
handler:^(UIAlertAction * action) {
|
||||
if (handler) {
|
||||
handler(self);
|
||||
}
|
||||
}];
|
||||
|
||||
[(UIAlertController *)_alert addAction:action];
|
||||
} else if ([_alert isKindOfClass:[UIActionSheet class]]) {
|
||||
if (actions == nil) {
|
||||
actions = [NSMutableArray array];
|
||||
}
|
||||
index = [(UIActionSheet *)_alert addButtonWithTitle:title];
|
||||
if (handler) {
|
||||
[actions addObject:handler];
|
||||
} else {
|
||||
[actions addObject:[NSNull null]];
|
||||
}
|
||||
} else if ([_alert isKindOfClass:[UIAlertView class]]) {
|
||||
if (actions == nil) {
|
||||
actions = [NSMutableArray array];
|
||||
}
|
||||
index = [(UIAlertView *)_alert addButtonWithTitle:title];
|
||||
if (handler) {
|
||||
[actions addObject:handler];
|
||||
} else {
|
||||
[actions addObject:[NSNull null]];
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
- (void)addTextFieldWithConfigurationHandler:(blockMXCAlert_textFieldHandler)configurationHandler {
|
||||
if ([_alert isKindOfClass:[UIAlertController class]]) {
|
||||
[(UIAlertController *)_alert addTextFieldWithConfigurationHandler:configurationHandler];
|
||||
} else if ([_alert isKindOfClass:[UIAlertView class]]) {
|
||||
UIAlertView *alertView = (UIAlertView *)_alert;
|
||||
// Check the current style
|
||||
if (alertView.alertViewStyle == UIAlertViewStyleDefault) {
|
||||
// Add the first text fields
|
||||
alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
|
||||
|
||||
if (configurationHandler) {
|
||||
// Store the callback
|
||||
UITextField *textField = [alertView textFieldAtIndex:0];
|
||||
objc_setAssociatedObject(textField, "configurationHandler", [configurationHandler copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
} else if (alertView.alertViewStyle != UIAlertViewStyleLoginAndPasswordInput) {
|
||||
// Add a second text field
|
||||
alertView.alertViewStyle = UIAlertViewStyleLoginAndPasswordInput;
|
||||
|
||||
if (configurationHandler) {
|
||||
// Store the callback
|
||||
UITextField *textField = [alertView textFieldAtIndex:1];
|
||||
objc_setAssociatedObject(textField, "configurationHandler", [configurationHandler copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
}
|
||||
// CAUTION 1: only 2 text fields are supported fro iOS < 8
|
||||
// CAUTION 2: alert style "UIAlertViewStyleSecureTextInput" is not supported, use the configurationHandler to handle secure text field
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showInViewController:(UIViewController*)viewController {
|
||||
if ([_alert isKindOfClass:[UIAlertController class]]) {
|
||||
if (viewController) {
|
||||
parentViewController = viewController;
|
||||
if (self.sourceView) {
|
||||
[_alert popoverPresentationController].sourceView = self.sourceView;
|
||||
[_alert popoverPresentationController].sourceRect = self.sourceView.bounds;
|
||||
}
|
||||
[viewController presentViewController:(UIAlertController *)_alert animated:YES completion:nil];
|
||||
}
|
||||
} else if ([_alert isKindOfClass:[UIActionSheet class]]) {
|
||||
[(UIActionSheet *)_alert showInView:[[UIApplication sharedApplication] keyWindow]];
|
||||
} else if ([_alert isKindOfClass:[UIAlertView class]]) {
|
||||
UIAlertView *alertView = (UIAlertView *)_alert;
|
||||
if (alertView.alertViewStyle != UIAlertViewStyleDefault) {
|
||||
// Call here textField handlers
|
||||
UITextField *textField = [alertView textFieldAtIndex:0];
|
||||
blockMXCAlert_textFieldHandler configurationHandler = objc_getAssociatedObject(textField, "configurationHandler");
|
||||
if (configurationHandler) {
|
||||
configurationHandler (textField);
|
||||
}
|
||||
if (alertView.alertViewStyle == UIAlertViewStyleLoginAndPasswordInput) {
|
||||
textField = [alertView textFieldAtIndex:1];
|
||||
blockMXCAlert_textFieldHandler configurationHandler = objc_getAssociatedObject(textField, "configurationHandler");
|
||||
if (configurationHandler) {
|
||||
configurationHandler (textField);
|
||||
}
|
||||
}
|
||||
}
|
||||
[alertView show];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismiss:(BOOL)animated {
|
||||
if ([_alert isKindOfClass:[UIAlertController class]]) {
|
||||
// only dismiss it if it is presented
|
||||
if (parentViewController.presentedViewController == _alert) {
|
||||
[parentViewController dismissViewControllerAnimated:animated completion:nil];
|
||||
}
|
||||
} else if ([_alert isKindOfClass:[UIActionSheet class]]) {
|
||||
[((UIActionSheet *)_alert) dismissWithClickedButtonIndex:self.cancelButtonIndex animated:animated];
|
||||
} else if ([_alert isKindOfClass:[UIAlertView class]]) {
|
||||
[((UIAlertView *)_alert) dismissWithClickedButtonIndex:self.cancelButtonIndex animated:animated];
|
||||
}
|
||||
_alert = nil;
|
||||
}
|
||||
|
||||
- (UITextField *)textFieldAtIndex:(NSInteger)textFieldIndex{
|
||||
if ([_alert isKindOfClass:[UIAlertController class]]) {
|
||||
return [((UIAlertController*)_alert).textFields objectAtIndex:textFieldIndex];
|
||||
} else if ([_alert isKindOfClass:[UIAlertView class]]) {
|
||||
return [((UIAlertView*)_alert) textFieldAtIndex:textFieldIndex];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - UIAlertViewDelegate (iOS < 8)
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
|
||||
// sanity check
|
||||
// the user could have forgotten to set the cancel button index
|
||||
if (buttonIndex < actions.count) {
|
||||
// Retrieve the callback
|
||||
blockMXCAlert_onClick block = [actions objectAtIndex:buttonIndex];
|
||||
if ([block isEqual:[NSNull null]] == NO) {
|
||||
// And call it
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
block(self);
|
||||
});
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Release alert reference
|
||||
_alert = nil;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIActionSheetDelegate (iOS < 8)
|
||||
|
||||
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
|
||||
|
||||
// sanity check
|
||||
// the user could have forgotten to set the cancel button index
|
||||
if (buttonIndex < actions.count) {
|
||||
// Retrieve the callback
|
||||
blockMXCAlert_onClick block = [actions objectAtIndex:buttonIndex];
|
||||
if ([block isEqual:[NSNull null]] == NO) {
|
||||
// And call it
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
block(self);
|
||||
});
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Release _alert reference
|
||||
_alert = nil;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/UIKit.h>
|
||||
#import <AddressBook/AddressBook.h>
|
||||
|
||||
// warn when a contact has a new matrix identifier
|
||||
// the contactID is provided in parameter
|
||||
extern NSString *const kMXCContactMatrixIdentifierUpdateNotification;
|
||||
// warn when the contact thumbnail is updated
|
||||
// the contactID is provided in parameter
|
||||
extern NSString *const kMXCContactThumbnailUpdateNotification;
|
||||
|
||||
@interface MXCContact : NSObject<NSCoding>
|
||||
|
||||
// unique identifier
|
||||
@property (nonatomic, readonly) NSString * contactID;
|
||||
// display name
|
||||
@property (nonatomic, readwrite) NSString *displayName;
|
||||
// contact thumbnail
|
||||
@property (nonatomic, copy, readonly) UIImage *thumbnail;
|
||||
// YES if the contact does not exist in the contacts book
|
||||
// the contact has been created from a MXUser
|
||||
@property (nonatomic, readonly) BOOL isMatrixContact;
|
||||
// array of MXCPhoneNumber
|
||||
@property (nonatomic, readonly) NSArray *phoneNumbers;
|
||||
// array of MXCEmail
|
||||
@property (nonatomic, readonly) NSArray *emailAddresses;
|
||||
// array of matrix identifiers
|
||||
@property (nonatomic, readonly) NSArray* matrixIdentifiers;
|
||||
|
||||
// return the contact ID from native phonebook record
|
||||
+ (NSString*)contactID:(ABRecordRef)record;
|
||||
|
||||
// create a contact from a local contact
|
||||
- (id)initWithABRecord:(ABRecordRef)record;
|
||||
|
||||
// create a contact with the dedicated info
|
||||
- (id)initWithDisplayName:(NSString*)displayName matrixID:(NSString*)matrixID;
|
||||
|
||||
// return thumbnail with a prefered size
|
||||
// if the thumbnail is already loaded, this method returns this one
|
||||
// if the thumbnail must trigger a server request, the expected size will be size
|
||||
// self.thumbnail triggered a request with a 256 X 256 pixels
|
||||
- (UIImage*)thumbnailWithPreferedSize:(CGSize)size;
|
||||
|
||||
// check if the patterns can match with this contact
|
||||
- (BOOL) matchedWithPatterns:(NSArray*)patterns;
|
||||
|
||||
// internationalize the contact phonenumbers
|
||||
- (void)internationalizePhonenumbers:(NSString*)countryCode;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "MXCContact.h"
|
||||
|
||||
#import "MXCEmail.h"
|
||||
#import "MXCPhoneNumber.h"
|
||||
|
||||
// warn when a contact has a new matrix identifier
|
||||
// the contactID is provided in parameter
|
||||
NSString *const kMXCContactMatrixIdentifierUpdateNotification = @"kMXCContactMatrixIdentifierUpdateNotification";
|
||||
|
||||
// warn when the contact thumbnail is updated
|
||||
// the contactID is provided in parameter
|
||||
NSString *const kMXCContactThumbnailUpdateNotification = @"kMXCContactThumbnailUpdateNotification";
|
||||
|
||||
@interface MXCContact() {
|
||||
UIImage* contactBookThumbnail;
|
||||
UIImage* matrixThumbnail;
|
||||
|
||||
// used when the contact is not defined in the contacts book
|
||||
MXCContactField * dummyField;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MXCContact
|
||||
|
||||
+ (NSString*)contactID:(ABRecordRef)record {
|
||||
return [NSString stringWithFormat:@"%d", ABRecordGetRecordID(record)];
|
||||
}
|
||||
|
||||
- (id) initWithABRecord:(ABRecordRef)record {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
// compute a contact ID
|
||||
_contactID = [MXCContact contactID:record];
|
||||
|
||||
// use the contact book display name
|
||||
_displayName = (__bridge NSString*) ABRecordCopyCompositeName(record);
|
||||
|
||||
// avoid nil display name
|
||||
// the display name is used to sort contacts
|
||||
if (!_displayName) {
|
||||
_displayName = @"";
|
||||
}
|
||||
|
||||
dummyField = nil;
|
||||
|
||||
// extract the phone numbers and their related label
|
||||
ABMultiValueRef multi = ABRecordCopyValue(record, kABPersonPhoneProperty);
|
||||
CFIndex nCount = ABMultiValueGetCount(multi);
|
||||
NSMutableArray* pns = [[NSMutableArray alloc] initWithCapacity:nCount];
|
||||
|
||||
for (int i = 0; i < nCount; i++) {
|
||||
CFTypeRef phoneRef = ABMultiValueCopyValueAtIndex(multi, i);
|
||||
NSString *phoneVal = (__bridge NSString*)phoneRef;
|
||||
|
||||
// sanity check
|
||||
if (0 != [phoneVal length]) {
|
||||
CFStringRef lblRef = ABMultiValueCopyLabelAtIndex(multi, i);
|
||||
CFStringRef localizedLblRef = nil;
|
||||
NSString *lbl = @"";
|
||||
|
||||
if (lblRef != nil) {
|
||||
localizedLblRef = ABAddressBookCopyLocalizedLabel(lblRef);
|
||||
if (localizedLblRef) {
|
||||
lbl = (__bridge NSString*)localizedLblRef;
|
||||
} else {
|
||||
lbl = (__bridge NSString*)lblRef;
|
||||
}
|
||||
} else {
|
||||
localizedLblRef = ABAddressBookCopyLocalizedLabel(kABOtherLabel);
|
||||
if (localizedLblRef) {
|
||||
lbl = (__bridge NSString*)localizedLblRef;
|
||||
}
|
||||
}
|
||||
|
||||
[pns addObject:[[MXCPhoneNumber alloc] initWithTextNumber:phoneVal type:lbl contactID:_contactID matrixID:nil]];
|
||||
|
||||
if (lblRef) {
|
||||
CFRelease(lblRef);
|
||||
}
|
||||
if (localizedLblRef) {
|
||||
CFRelease(localizedLblRef);
|
||||
}
|
||||
}
|
||||
|
||||
// release meory
|
||||
if (phoneRef) {
|
||||
CFRelease(phoneRef);
|
||||
}
|
||||
}
|
||||
|
||||
CFRelease(multi);
|
||||
_phoneNumbers = pns;
|
||||
|
||||
// extract the emails
|
||||
multi = ABRecordCopyValue(record, kABPersonEmailProperty);
|
||||
nCount = ABMultiValueGetCount(multi);
|
||||
|
||||
NSMutableArray *emails = [[NSMutableArray alloc] initWithCapacity:nCount];
|
||||
|
||||
for (int i = 0; i < nCount; i++) {
|
||||
CFTypeRef emailValRef = ABMultiValueCopyValueAtIndex(multi, i);
|
||||
NSString *emailVal = (__bridge NSString*)emailValRef;
|
||||
|
||||
// sanity check
|
||||
if ((nil != emailVal) && (0 != [emailVal length])) {
|
||||
CFStringRef lblRef = ABMultiValueCopyLabelAtIndex(multi, i);
|
||||
CFStringRef localizedLblRef = nil;
|
||||
NSString *lbl = @"";
|
||||
|
||||
if (lblRef != nil) {
|
||||
localizedLblRef = ABAddressBookCopyLocalizedLabel(lblRef);
|
||||
|
||||
if (localizedLblRef) {
|
||||
lbl = (__bridge NSString*)localizedLblRef;
|
||||
}
|
||||
else {
|
||||
lbl = (__bridge NSString*)lblRef;
|
||||
}
|
||||
} else {
|
||||
localizedLblRef = ABAddressBookCopyLocalizedLabel(kABOtherLabel);
|
||||
if (localizedLblRef) {
|
||||
lbl = (__bridge NSString*)localizedLblRef;
|
||||
}
|
||||
}
|
||||
|
||||
[emails addObject: [[MXCEmail alloc] initWithEmailAddress:emailVal type:lbl contactID:_contactID matrixID:nil]];
|
||||
|
||||
if (lblRef) {
|
||||
CFRelease(lblRef);
|
||||
}
|
||||
|
||||
if (localizedLblRef) {
|
||||
CFRelease(localizedLblRef);
|
||||
}
|
||||
}
|
||||
|
||||
if (emailValRef) {
|
||||
CFRelease(emailValRef);
|
||||
}
|
||||
}
|
||||
|
||||
CFRelease(multi);
|
||||
|
||||
_emailAddresses = emails;
|
||||
|
||||
// thumbnail/picture
|
||||
// check whether the contact has a picture
|
||||
if (ABPersonHasImageData(record))
|
||||
{
|
||||
CFDataRef dataRef;
|
||||
|
||||
dataRef = ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
|
||||
if (dataRef)
|
||||
{
|
||||
contactBookThumbnail = [UIImage imageWithData:(__bridge NSData*)dataRef];
|
||||
CFRelease(dataRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// create a contact with the dedicated info
|
||||
- (id)initWithDisplayName:(NSString*)aDisplayName matrixID:(NSString*)matrixID
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_contactID = [[NSUUID UUID] UUIDString];
|
||||
|
||||
// used when the contact is not defined in the contacts book
|
||||
dummyField = [[MXCContactField alloc] initWithContactID:_contactID matrixID:matrixID];
|
||||
|
||||
// _displayName must not be nil
|
||||
// it is used to sort the contacts
|
||||
if (aDisplayName) {
|
||||
_displayName = aDisplayName;
|
||||
} else {
|
||||
_displayName = @"";
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// check if the patterns can match with this contact
|
||||
- (BOOL) matchedWithPatterns:(NSArray*)patterns {
|
||||
|
||||
BOOL matched = NO;
|
||||
|
||||
if (patterns.count > 0) {
|
||||
|
||||
matched = YES;
|
||||
|
||||
// test first display name
|
||||
for(NSString* pattern in patterns) {
|
||||
if ([_displayName rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound) {
|
||||
matched = NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched && _phoneNumbers.count > 0) {
|
||||
for(MXCPhoneNumber* phonenumber in _phoneNumbers) {
|
||||
if ([phonenumber matchedWithPatterns:patterns]) {
|
||||
matched = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched && _emailAddresses.count > 0) {
|
||||
for(MXCEmail* email in _emailAddresses) {
|
||||
if ([email matchedWithPatterns:patterns]) {
|
||||
matched = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if there is no pattern to search, it should always matched
|
||||
matched = YES;
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
// internationalize the contact phonenumbers
|
||||
- (void)internationalizePhonenumbers:(NSString*)countryCode {
|
||||
for(MXCPhoneNumber* phonenumber in _phoneNumbers) {
|
||||
phonenumber.countryCode = countryCode;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - getter/setter
|
||||
|
||||
- (BOOL) isMatrixContact {
|
||||
return (nil != dummyField);
|
||||
}
|
||||
|
||||
- (NSArray*) matrixIdentifiers {
|
||||
NSMutableArray* identifiers = [[NSMutableArray alloc] init];
|
||||
|
||||
if (dummyField) {
|
||||
[identifiers addObject:dummyField.matrixID];
|
||||
}
|
||||
|
||||
for(MXCEmail* email in _emailAddresses) {
|
||||
if (email.matrixID && ([identifiers indexOfObject:email.matrixID] == NSNotFound)) {
|
||||
[identifiers addObject:email.matrixID];
|
||||
}
|
||||
}
|
||||
|
||||
for(MXCPhoneNumber* pn in _phoneNumbers) {
|
||||
if (pn.matrixID && ([identifiers indexOfObject:pn.matrixID] == NSNotFound)) {
|
||||
[identifiers addObject:pn.matrixID];
|
||||
}
|
||||
}
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
- (void)setDisplayName:(NSString *)displayName {
|
||||
// a display name must not be emptied
|
||||
// it is used to sort the contacts
|
||||
if (displayName.length == 0) {
|
||||
_displayName = _contactID;
|
||||
} else {
|
||||
_displayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
// return thumbnail with a prefered size
|
||||
// if the thumbnail is already loaded, this method returns this one
|
||||
// if the thumbnail must trigger a server request, the expected size will be size
|
||||
// self.thumbnail triggered a request with a 256 X 256 pixels
|
||||
- (UIImage*)thumbnailWithPreferedSize:(CGSize)size {
|
||||
// already found a matrix thumbnail
|
||||
if (matrixThumbnail) {
|
||||
return matrixThumbnail;
|
||||
} else {
|
||||
//
|
||||
MXCContactField* firstField = dummyField;
|
||||
|
||||
if (firstField) {
|
||||
if (firstField.avatarImage) {
|
||||
matrixThumbnail = firstField.avatarImage;
|
||||
return matrixThumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
// try to replace the thumbnail by the matrix one
|
||||
if (_emailAddresses.count > 0) {
|
||||
// list the linked email
|
||||
// search if one email field has a dedicated thumbnail
|
||||
for(MXCEmail* email in _emailAddresses) {
|
||||
if (email.avatarImage) {
|
||||
matrixThumbnail = email.avatarImage;
|
||||
return matrixThumbnail;
|
||||
} else if (!firstField) {
|
||||
firstField = email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no thumbnail has been found
|
||||
// try to load the first field one
|
||||
if (firstField) {
|
||||
// should be retrieved by the cell info
|
||||
[firstField loadAvatarWithSize:size];
|
||||
}
|
||||
|
||||
return contactBookThumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
- (UIImage*)thumbnail {
|
||||
return [self thumbnailWithPreferedSize:CGSizeMake(256, 256)];
|
||||
}
|
||||
|
||||
#pragma mark NSCoding
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
_contactID = [coder decodeObjectForKey:@"contactID"];
|
||||
_displayName = [coder decodeObjectForKey:@"displayName"];
|
||||
|
||||
_phoneNumbers = [coder decodeObjectForKey:@"phoneNumbers"];
|
||||
_emailAddresses = [coder decodeObjectForKey:@"emailAddresses"];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
|
||||
[coder encodeObject:_contactID forKey:@"contactID"];
|
||||
[coder encodeObject:_displayName forKey:@"displayName"];
|
||||
|
||||
if (_phoneNumbers) {
|
||||
[coder encodeObject:_phoneNumbers forKey:@"phoneNumbers"];
|
||||
} else {
|
||||
[coder setNilValueForKey:@"phoneNumbers"];
|
||||
}
|
||||
|
||||
if (_emailAddresses) {
|
||||
[coder encodeObject:_emailAddresses forKey:@"emailAddresses"];
|
||||
} else {
|
||||
[coder setNilValueForKey:@"emailAddresses"];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/UIKit.h>
|
||||
|
||||
@interface MXCContactField : NSObject<NSCoding>
|
||||
|
||||
// contact ID where the email has been found
|
||||
@property (nonatomic, readonly) NSString *contactID;
|
||||
// linked matrix account
|
||||
@property (nonatomic, readwrite) NSString *matrixID;
|
||||
@property (nonatomic, readonly) UIImage *avatarImage;
|
||||
|
||||
- (id)initWithContactID:(NSString*)contactID matrixID:(NSString*)matrixID;
|
||||
|
||||
- (void)loadAvatarWithSize:(CGSize)avatarSize;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "MXCContactField.h"
|
||||
|
||||
// wanr when there is a contact update
|
||||
#import "MXCContact.h"
|
||||
|
||||
// image URL
|
||||
#import "MatrixSDKHandler.h"
|
||||
|
||||
// get the thumbnail
|
||||
#import "MediaManager.h"
|
||||
|
||||
|
||||
@interface MXCContactField() {
|
||||
NSString* avatarURL;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MXCContactField
|
||||
|
||||
- (void)initFields {
|
||||
// init members
|
||||
_contactID = nil;
|
||||
_matrixID = nil;
|
||||
avatarURL = @"";
|
||||
}
|
||||
|
||||
- (id)initWithContactID:(NSString*)contactID matrixID:(NSString*)matrixID {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
[self initFields];
|
||||
_contactID = contactID;
|
||||
_matrixID = matrixID;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
- (void)dealloc {
|
||||
// remove the observers
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setMatrixID:(NSString*)aMatrixID {
|
||||
// check if there is an update
|
||||
// nil test + string comparison
|
||||
if ((aMatrixID != _matrixID) && ![aMatrixID isEqualToString:_matrixID]) {
|
||||
_matrixID = aMatrixID;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kMXCContactMatrixIdentifierUpdateNotification object:_contactID userInfo:nil];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadAvatarWithSize:(CGSize)avatarSize {
|
||||
|
||||
// the avatar image is already done
|
||||
if (_avatarImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (_matrixID) {
|
||||
|
||||
// nil -> there is no avatar
|
||||
if (!avatarURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// empty string means not yet initialized
|
||||
if (avatarURL.length > 0) {
|
||||
[self downloadAvatarImage];
|
||||
} else {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
// check if the user is already known
|
||||
MXUser* user = [mxHandler.mxSession userWithUserId:_matrixID];
|
||||
|
||||
if (user) {
|
||||
avatarURL = [mxHandler thumbnailURLForContent:user.avatarUrl inViewSize:avatarSize withMethod:MXThumbnailingMethodCrop];
|
||||
[self downloadAvatarImage];
|
||||
|
||||
} else {
|
||||
|
||||
if (mxHandler.mxRestClient) {
|
||||
[mxHandler.mxRestClient avatarUrlForUser:_matrixID
|
||||
success:^(NSString *avatarUrl) {
|
||||
avatarURL = [mxHandler thumbnailURLForContent:avatarUrl inViewSize:avatarSize withMethod:MXThumbnailingMethodCrop];
|
||||
[self downloadAvatarImage];
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
//
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)downloadAvatarImage {
|
||||
|
||||
// the avatar image is already done
|
||||
if (_avatarImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (avatarURL.length > 0) {
|
||||
|
||||
_avatarImage = [MediaManager loadCachePictureForURL:avatarURL inFolder:kMediaManagerThumbnailFolder];
|
||||
|
||||
// the image is already in the cache
|
||||
if (_avatarImage) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kMXCContactThumbnailUpdateNotification object:_contactID userInfo:nil];
|
||||
});
|
||||
} else {
|
||||
MediaLoader* loader = [MediaManager existingDownloaderForURL:avatarURL inFolder:kMediaManagerThumbnailFolder];
|
||||
|
||||
if (!loader) {
|
||||
[MediaManager downloadMediaFromURL:avatarURL withType:@"image/jpeg" inFolder:kMediaManagerThumbnailFolder];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onMediaDownloadEnd:(NSNotification *)notif {
|
||||
// sanity check
|
||||
if ([notif.object isKindOfClass:[NSString class]]) {
|
||||
NSString* url = notif.object;
|
||||
|
||||
if ([url isEqualToString:avatarURL]) {
|
||||
// update the image
|
||||
UIImage* image = [MediaManager loadCachePictureForURL:avatarURL inFolder:kMediaManagerThumbnailFolder];
|
||||
if (image) {
|
||||
_avatarImage = image;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kMXCContactThumbnailUpdateNotification object:_contactID userInfo:nil];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark NSCoding
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if (self) {
|
||||
[self initFields];
|
||||
_contactID = [coder decodeObjectForKey:@"contactID"];
|
||||
_matrixID = [coder decodeObjectForKey:@"matrixID"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[coder encodeObject:_contactID forKey:@"contactID"];
|
||||
[coder encodeObject:_matrixID forKey:@"matrixID"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/UIKit.h>
|
||||
#import "MXCContactField.h"
|
||||
|
||||
@interface MXCEmail : MXCContactField
|
||||
|
||||
// email info
|
||||
@property (nonatomic, readonly) NSString *type;
|
||||
@property (nonatomic, readonly) NSString *emailAddress;
|
||||
|
||||
- (id)initWithEmailAddress:(NSString*)anEmailAddress type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID;
|
||||
|
||||
- (BOOL)matchedWithPatterns:(NSArray*)patterns;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "MXCEmail.h"
|
||||
#import "MatrixSDKHandler.h"
|
||||
|
||||
#import "MXCContact.h"
|
||||
|
||||
#import "MediaManager.h"
|
||||
|
||||
@interface MXCEmail()
|
||||
@end
|
||||
|
||||
@implementation MXCEmail
|
||||
|
||||
- (void) commonInit {
|
||||
// init members
|
||||
_emailAddress = nil;
|
||||
_type = nil;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithEmailAddress:(NSString*)anEmailAddress type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID {
|
||||
self = [super initWithContactID:aContactID matrixID:matrixID];
|
||||
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
_emailAddress = anEmailAddress;
|
||||
_type = aType;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)matchedWithPatterns:(NSArray*)patterns {
|
||||
// no number -> cannot match
|
||||
if (_emailAddress.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
if (patterns.count > 0) {
|
||||
for(NSString *pattern in patterns) {
|
||||
if ([_emailAddress rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
#pragma mark NSCoding
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder {
|
||||
self = [super initWithCoder:coder];
|
||||
|
||||
if (self) {
|
||||
_type = [coder decodeObjectForKey:@"type"];
|
||||
_emailAddress = [coder decodeObjectForKey:@"emailAddress"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[super encodeWithCoder:coder];
|
||||
|
||||
[coder encodeObject:_type forKey:@"type"];
|
||||
[coder encodeObject:_emailAddress forKey:@"emailAddress"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/Foundation.h>
|
||||
|
||||
#import "MXCContactField.h"
|
||||
|
||||
@interface MXCPhoneNumber : MXCContactField
|
||||
|
||||
// phonenumber info
|
||||
@property (nonatomic, readonly) NSString *type;
|
||||
@property (nonatomic, readonly) NSString *textNumber;
|
||||
@property (nonatomic, readonly) NSString *internationalPhoneNumber;
|
||||
// defines a country code will internationalize it
|
||||
@property (nonatomic, readwrite) NSString *countryCode;
|
||||
@property (nonatomic, readonly) BOOL isValid;
|
||||
|
||||
- (id)initWithTextNumber:(NSString*)textNumber type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID;
|
||||
|
||||
- (BOOL)matchedWithPatterns:(NSArray*)patterns;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "MXCPhoneNumber.h"
|
||||
|
||||
#import "NBPhoneNumberUtil.h"
|
||||
|
||||
@interface MXCPhoneNumber ()
|
||||
@property (nonatomic, readonly) NSString *cleanedPhonenumber;
|
||||
@end
|
||||
|
||||
@implementation MXCPhoneNumber
|
||||
|
||||
- (id)initWithTextNumber:(NSString*)aTextNumber type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID {
|
||||
self = [super initWithContactID:aContactID matrixID:matrixID];
|
||||
|
||||
if (self) {
|
||||
_type = aType ? aType : @"";
|
||||
_textNumber = aTextNumber ? aTextNumber : @"" ;
|
||||
_cleanedPhonenumber = [MXCPhoneNumber cleanPhonenumber:_textNumber];
|
||||
_internationalPhoneNumber = nil;
|
||||
_countryCode = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// remove the unuseful characters in a phonenumber
|
||||
+ (NSString*) cleanPhonenumber:(NSString*)phoneNumber {
|
||||
|
||||
// sanity check
|
||||
if (nil == phoneNumber)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
// empty string
|
||||
if (0 == [phoneNumber length])
|
||||
{
|
||||
return @"";
|
||||
}
|
||||
|
||||
static NSCharacterSet *invertedPhoneCharSet = nil;
|
||||
|
||||
if (!invertedPhoneCharSet)
|
||||
{
|
||||
invertedPhoneCharSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789+*#,"] invertedSet];
|
||||
}
|
||||
|
||||
return [[phoneNumber componentsSeparatedByCharactersInSet:invertedPhoneCharSet] componentsJoinedByString:@""];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)matchedWithPatterns:(NSArray*)patterns {
|
||||
// no number -> cannot match
|
||||
if (_textNumber.length == 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (patterns.count > 0) {
|
||||
for(NSString *pattern in patterns) {
|
||||
if (([_textNumber rangeOfString:pattern].location == NSNotFound) && ([_cleanedPhonenumber rangeOfString:pattern].location == NSNotFound)) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)setCountryCode:(NSString *)aCountryCode {
|
||||
if (![aCountryCode isEqualToString:_countryCode]) {
|
||||
_internationalPhoneNumber = nil;
|
||||
_countryCode = aCountryCode;
|
||||
|
||||
NSError* error = nil;
|
||||
NBPhoneNumberUtil *phoneUtil = [NBPhoneNumberUtil sharedInstance];
|
||||
NBPhoneNumber* nbPhoneNumber = [phoneUtil parse:_cleanedPhonenumber defaultRegion:aCountryCode error:&error];
|
||||
|
||||
if (!error && [phoneUtil isValidNumber:nbPhoneNumber])
|
||||
{
|
||||
NSString* e164Number = [phoneUtil format:nbPhoneNumber numberFormat:NBEPhoneNumberFormatE164 error:&error];
|
||||
|
||||
if (!error && (e164Number.length > 0))
|
||||
{
|
||||
// need to plug to libphonenumber
|
||||
_internationalPhoneNumber = e164Number;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isValidPhonenumber {
|
||||
if (_countryCode) {
|
||||
return (nil != _internationalPhoneNumber);
|
||||
} else {
|
||||
NSError* error = nil;
|
||||
NBPhoneNumberUtil *phoneUtil = [NBPhoneNumberUtil sharedInstance];
|
||||
NBPhoneNumber* nbPhoneNumber = [phoneUtil parse:_cleanedPhonenumber defaultRegion:nil error:&error];
|
||||
|
||||
return (!error && [phoneUtil isValidNumber:nbPhoneNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark NSCoding
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder {
|
||||
self = [super initWithCoder:coder];
|
||||
|
||||
if (self) {
|
||||
_type = [coder decodeObjectForKey:@"type"];
|
||||
_textNumber = [coder decodeObjectForKey:@"textNumber"];
|
||||
_cleanedPhonenumber = [coder decodeObjectForKey:@"cleanedPhonenumber"];
|
||||
_internationalPhoneNumber = [coder decodeObjectForKey:@"internationalPhoneNumber"];
|
||||
_countryCode = [coder decodeObjectForKey:@"countryCode"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||
[super encodeWithCoder:coder];
|
||||
|
||||
[coder encodeObject:_type forKey:@"type"];
|
||||
[coder encodeObject:_textNumber forKey:@"textNumber"];
|
||||
[coder encodeObject:_cleanedPhonenumber forKey:@"cleanedPhonenumber"];
|
||||
[coder encodeObject:_internationalPhoneNumber forKey:@"internationalPhoneNumber"];
|
||||
[coder encodeObject:_countryCode forKey:@"countryCode"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/UIKit.h>
|
||||
|
||||
#import <MessageUI/MessageUI.h>
|
||||
|
||||
@interface RageShakableUIResponder : UIResponder<MFMailComposeViewControllerDelegate>
|
||||
|
||||
+ (void)startShaking:(UIResponder*)controller;
|
||||
+ (void)stopShaking:(UIResponder*)controller;
|
||||
+ (void)cancel:(UIResponder*)controller;
|
||||
+ (void)applicationBecomesActive;
|
||||
+ (void)reportCrash:(UIViewController*)viewController;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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.
|
||||
*/
|
||||
|
||||
#define RAGESHAKABLEUIRESPONDER_MINIMUM_SHAKING_DURATION 2
|
||||
|
||||
#import "RageShakableUIResponder.h"
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "MXCAlert.h"
|
||||
#import "MatrixSDKHandler.h"
|
||||
|
||||
#import "GBDeviceInfo_iOS.h"
|
||||
|
||||
@interface RageShakableUIResponder() {
|
||||
MXCAlert *confirmationAlert;
|
||||
double startShakingTimeStamp;
|
||||
bool isShaking;
|
||||
bool ignoreShakeEnd;
|
||||
|
||||
UIViewController* parentViewController;
|
||||
MFMailComposeViewController* mailComposer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation RageShakableUIResponder
|
||||
|
||||
static RageShakableUIResponder* sharedInstance = nil;
|
||||
|
||||
- (id) init {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
mailComposer = nil;
|
||||
confirmationAlert = nil;
|
||||
startShakingTimeStamp = 0;
|
||||
isShaking = NO;
|
||||
ignoreShakeEnd = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (void)startShaking:(UIResponder*)responder {
|
||||
if (!sharedInstance) {
|
||||
sharedInstance = [[RageShakableUIResponder alloc] init];
|
||||
}
|
||||
|
||||
RageShakableUIResponder* rageShakableUIResponder = [responder isKindOfClass:[RageShakableUIResponder class]] ? (RageShakableUIResponder*)responder : sharedInstance;
|
||||
|
||||
// only start if the application is in foreground
|
||||
if ([AppDelegate theDelegate].isAppForeground && !rageShakableUIResponder->confirmationAlert) {
|
||||
NSLog(@"[RageShake] Start shaking with [%@]", [responder class]);
|
||||
|
||||
rageShakableUIResponder->startShakingTimeStamp = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
rageShakableUIResponder->isShaking = YES;
|
||||
rageShakableUIResponder->ignoreShakeEnd = NO;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)stopShaking:(UIResponder*)responder {
|
||||
if (!sharedInstance) {
|
||||
sharedInstance = [[RageShakableUIResponder alloc] init];
|
||||
}
|
||||
|
||||
NSLog(@"[RageShake] Stop shaking with [%@]", [responder class]);
|
||||
|
||||
RageShakableUIResponder* rageShakableUIResponder = [responder isKindOfClass:[RageShakableUIResponder class]] ? (RageShakableUIResponder*)responder : sharedInstance;
|
||||
|
||||
if (rageShakableUIResponder && [AppDelegate theDelegate].isAppForeground && (([[NSDate date] timeIntervalSince1970] - rageShakableUIResponder->startShakingTimeStamp) > RAGESHAKABLEUIRESPONDER_MINIMUM_SHAKING_DURATION) && !rageShakableUIResponder->confirmationAlert) {
|
||||
if (!rageShakableUIResponder->ignoreShakeEnd) {
|
||||
rageShakableUIResponder->startShakingTimeStamp = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
if ([responder isKindOfClass:[UIViewController class]]) {
|
||||
rageShakableUIResponder->confirmationAlert = [[MXCAlert alloc] initWithTitle:@"You seem to be shaking the phone in frustration. Would you like to submit a bug report?" message:nil style:MXCAlertStyleAlert];
|
||||
|
||||
[rageShakableUIResponder->confirmationAlert addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
||||
sharedInstance->confirmationAlert = nil;
|
||||
}];
|
||||
|
||||
[rageShakableUIResponder->confirmationAlert addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
||||
sharedInstance->confirmationAlert = nil;
|
||||
[RageShakableUIResponder sendEmail:(UIViewController*)responder withSnapshot:YES];
|
||||
}];
|
||||
|
||||
[rageShakableUIResponder->confirmationAlert showInViewController:(UIViewController*)responder];
|
||||
}
|
||||
} else {
|
||||
[RageShakableUIResponder sendEmail:nil withSnapshot:NO];
|
||||
}
|
||||
}
|
||||
|
||||
rageShakableUIResponder->isShaking = NO;
|
||||
rageShakableUIResponder->ignoreShakeEnd = NO;
|
||||
}
|
||||
|
||||
+ (void)cancel:(UIResponder*)responder {
|
||||
|
||||
if (!sharedInstance) {
|
||||
sharedInstance = [[RageShakableUIResponder alloc] init];
|
||||
}
|
||||
|
||||
RageShakableUIResponder* rageShakableUIResponder = [responder isKindOfClass:[RageShakableUIResponder class]] ? (RageShakableUIResponder*)responder : sharedInstance;
|
||||
|
||||
// Arathorn succeeded to shake the device and to put the application in background at the same time (magic finders)
|
||||
// it should prevent any screenshot alert in this crazy case
|
||||
rageShakableUIResponder->startShakingTimeStamp = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
if (rageShakableUIResponder->isShaking) {
|
||||
rageShakableUIResponder->ignoreShakeEnd = YES;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)reportCrash:(UIViewController*)viewController {
|
||||
if ([MXLogger crashLog]) {
|
||||
if (!sharedInstance) {
|
||||
sharedInstance = [[RageShakableUIResponder alloc] init];
|
||||
}
|
||||
|
||||
sharedInstance->confirmationAlert = [[MXCAlert alloc] initWithTitle:@"The application has crashed last time. Would you like to submit a crash report?" message:nil style:MXCAlertStyleAlert];
|
||||
|
||||
[sharedInstance->confirmationAlert addActionWithTitle:@"Cancel" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
||||
// Erase the crash log (there is only chance for the user to send it)
|
||||
[MXLogger deleteCrashLog];
|
||||
sharedInstance->confirmationAlert = nil;
|
||||
}];
|
||||
|
||||
[sharedInstance->confirmationAlert addActionWithTitle:@"OK" style:MXCAlertActionStyleDefault handler:^(MXCAlert *alert) {
|
||||
sharedInstance->confirmationAlert = nil;
|
||||
[RageShakableUIResponder sendEmail:viewController withSnapshot:NO];
|
||||
}];
|
||||
|
||||
[sharedInstance->confirmationAlert showInViewController:viewController];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)applicationBecomesActive {
|
||||
[RageShakableUIResponder cancel:nil];
|
||||
}
|
||||
|
||||
// Prepare and send a report email
|
||||
// If `snapshot` is YES, a screenshot of `controller` will be sent as image attachment to the email
|
||||
+ (void)sendEmail:(UIViewController*)controller withSnapshot:(BOOL)snapshot {
|
||||
|
||||
UIImage *image;
|
||||
|
||||
if (snapshot) {
|
||||
AppDelegate* theDelegate = [AppDelegate theDelegate];
|
||||
UIGraphicsBeginImageContextWithOptions(theDelegate.window.bounds.size, NO, [UIScreen mainScreen].scale);
|
||||
|
||||
// Iterate over every window from back to front
|
||||
for (UIWindow *window in [[UIApplication sharedApplication] windows])
|
||||
{
|
||||
if (![window respondsToSelector:@selector(screen)] || [window screen] == [UIScreen mainScreen])
|
||||
{
|
||||
// -renderInContext: renders in the coordinate space of the layer,
|
||||
// so we must first apply the layer's geometry to the graphics context
|
||||
CGContextSaveGState(UIGraphicsGetCurrentContext());
|
||||
// Center the context around the window's anchor point
|
||||
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), [window center].x, [window center].y);
|
||||
// Apply the window's transform about the anchor point
|
||||
CGContextConcatCTM(UIGraphicsGetCurrentContext(), [window transform]);
|
||||
// Offset by the portion of the bounds left of and above the anchor point
|
||||
CGContextTranslateCTM(UIGraphicsGetCurrentContext(),
|
||||
-[window bounds].size.width * [[window layer] anchorPoint].x,
|
||||
-[window bounds].size.height * [[window layer] anchorPoint].y);
|
||||
|
||||
// Render the layer hierarchy to the current context
|
||||
[[window layer] renderInContext:UIGraphicsGetCurrentContext()];
|
||||
|
||||
// Restore the context
|
||||
CGContextRestoreGState(UIGraphicsGetCurrentContext());
|
||||
}
|
||||
}
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
// the image is copied in the clipboard
|
||||
[UIPasteboard generalPasteboard].image = image;
|
||||
}
|
||||
|
||||
if (controller) {
|
||||
[controller.view snapshotViewAfterScreenUpdates:YES];
|
||||
|
||||
sharedInstance->parentViewController = controller;
|
||||
sharedInstance->mailComposer = [[MFMailComposeViewController alloc] init];
|
||||
|
||||
if ([MXLogger crashLog]) {
|
||||
[sharedInstance->mailComposer setSubject:@"Matrix crash report"];
|
||||
}
|
||||
else {
|
||||
[sharedInstance->mailComposer setSubject:@"Matrix bug report"];
|
||||
}
|
||||
|
||||
[sharedInstance->mailComposer setToRecipients:[NSArray arrayWithObject:@"rageshake@matrix.org"]];
|
||||
|
||||
NSString* appVersion = [AppDelegate theDelegate].appVersion;
|
||||
NSString* build = [AppDelegate theDelegate].build;
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
NSMutableString* message = [[NSMutableString alloc] init];
|
||||
|
||||
[message appendFormat:@"Something went wrong on my Matrix client: \n\n\n"];
|
||||
|
||||
[message appendFormat:@"-----> my comments <-----\n\n\n"];
|
||||
|
||||
[message appendFormat:@"------------------------------\n"];
|
||||
[message appendFormat:@"Application info\n"];
|
||||
[message appendFormat:@"userId: %@\n", mxHandler.userId];
|
||||
[message appendFormat:@"displayname: %@\n", mxHandler.mxSession.myUser.displayname];
|
||||
[message appendFormat:@"\n"];
|
||||
[message appendFormat:@"homeServerURL: %@\n", mxHandler.homeServerURL];
|
||||
[message appendFormat:@"homeServer: %@\n", mxHandler.homeServer];
|
||||
[message appendFormat:@"\n"];
|
||||
[message appendFormat:@"matrixConsole version: %@\n", appVersion];
|
||||
[message appendFormat:@"SDK version: %@\n", MatrixSDKVersion];
|
||||
if (build.length) {
|
||||
[message appendFormat:@"Build: %@\n", build];
|
||||
}
|
||||
[message appendFormat:@"------------------------------\n"];
|
||||
[message appendFormat:@"Device info\n"];
|
||||
[message appendFormat:@"model: %@\n", [GBDeviceInfo deviceDetails].modelString];
|
||||
[message appendFormat:@"operatingSystem: %@ %@\n", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion]];
|
||||
|
||||
[sharedInstance->mailComposer setMessageBody:message isHTML:NO];
|
||||
|
||||
// Attach image only if required
|
||||
if (image) {
|
||||
[sharedInstance->mailComposer addAttachmentData:UIImageJPEGRepresentation(image, 1.0) mimeType:@"image/jpg" fileName:@"screenshot.jpg"];
|
||||
}
|
||||
|
||||
// Add logs files
|
||||
NSMutableArray *logFiles = [NSMutableArray arrayWithArray:[MXLogger logFiles]];
|
||||
if ([MXLogger crashLog]) {
|
||||
[logFiles addObject:[MXLogger crashLog]];
|
||||
}
|
||||
for (NSString *logFile in logFiles) {
|
||||
NSData *logContent = [NSData dataWithContentsOfFile:logFile];
|
||||
[sharedInstance->mailComposer addAttachmentData:logContent mimeType:@"text/plain" fileName:[logFile lastPathComponent]];
|
||||
}
|
||||
sharedInstance->mailComposer.mailComposeDelegate = sharedInstance;
|
||||
[controller presentViewController:sharedInstance->mailComposer animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MFMailComposeViewControllerDelegate delegate
|
||||
- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {
|
||||
// Do not send this crash anymore
|
||||
[MXLogger deleteCrashLog];
|
||||
[controller dismissViewControllerAnimated:NO completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 <MatrixSDK/MatrixSDK.h>
|
||||
|
||||
// When a recent is initialized with a blank last event description (unexpected/unsupported event),
|
||||
// a back pagination is triggered to find a non empty description.
|
||||
// The following notification is posted when this operation succeeds
|
||||
extern NSString *const kRecentRoomUpdatedByBackPagination;
|
||||
|
||||
@interface RecentRoom : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSString *roomId;
|
||||
@property (nonatomic, readonly) NSString *lastEventDescription;
|
||||
@property (nonatomic, readonly) uint64_t lastEventOriginServerTs;
|
||||
@property (nonatomic, readonly) NSUInteger unreadCount;
|
||||
@property (nonatomic, readonly) BOOL containsBingUnread;
|
||||
|
||||
- (id)initWithLastEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState markAsUnread:(BOOL)isUnread;
|
||||
// Update the current last event description with the provided event, except if this description is empty (see unsupported/unexpected events).
|
||||
// Return true when the provided event is considered as new last event
|
||||
- (BOOL)updateWithLastEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState markAsUnread:(BOOL)isUnread;
|
||||
- (void)resetUnreadCount;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "RecentRoom.h"
|
||||
#import "MatrixSDKHandler.h"
|
||||
|
||||
NSString *const kRecentRoomUpdatedByBackPagination = @"kRecentRoomUpdatedByBackPagination";
|
||||
|
||||
@interface RecentRoom() {
|
||||
MXRoom *mxRoom;
|
||||
id backPaginationListener;
|
||||
MXHTTPOperation *backPaginationOperation;
|
||||
|
||||
// Keep reference on last event (used in case of redaction)
|
||||
MXEvent *lastEvent;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation RecentRoom
|
||||
|
||||
- (id)initWithLastEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState markAsUnread:(BOOL)isUnread {
|
||||
if (self = [super init]) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
_roomId = event.roomId;
|
||||
_lastEventDescription = [mxHandler displayTextForEvent:event withRoomState:roomState inSubtitleMode:YES];
|
||||
_lastEventOriginServerTs = event.originServerTs;
|
||||
_unreadCount = isUnread ? 1 : 0;
|
||||
|
||||
// In case of unread, check whether the last event description contains bing words
|
||||
_containsBingUnread = (isUnread && !event.isState && !event.redactedBecause && [mxHandler containsBingWord:_lastEventDescription]);
|
||||
|
||||
// Keep ref on event
|
||||
lastEvent = event;
|
||||
|
||||
if (!_lastEventDescription.length) {
|
||||
// Trigger back pagination to get an event with a non empty description
|
||||
[self triggerBackPagination];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)updateWithLastEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState markAsUnread:(BOOL)isUnread {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
// Check whether the description of the provided event is not empty
|
||||
NSString *description = [mxHandler displayTextForEvent:event withRoomState:roomState inSubtitleMode:YES];
|
||||
if (description.length) {
|
||||
[self cancelBackPagination];
|
||||
// Update current last event
|
||||
lastEvent = event;
|
||||
_lastEventDescription = description;
|
||||
_lastEventOriginServerTs = event.originServerTs;
|
||||
if (isUnread) {
|
||||
_unreadCount ++;
|
||||
_containsBingUnread = (_containsBingUnread || (!event.isState && !event.redactedBecause && [mxHandler containsBingWord:_lastEventDescription]));
|
||||
}
|
||||
return YES;
|
||||
} else if (_lastEventDescription.length) {
|
||||
// Here we tried to update the last event with a new live one, but the description of this new one is empty.
|
||||
// Consider the specific case of redaction event
|
||||
if (event.eventType == MXEventTypeRoomRedaction) {
|
||||
// Check whether the redacted event is the current last event
|
||||
if ([event.redacts isEqualToString:lastEvent.eventId]) {
|
||||
// Update last event description
|
||||
MXEvent *redactedEvent = [lastEvent prune];
|
||||
redactedEvent.redactedBecause = event.originalDictionary;
|
||||
|
||||
_lastEventDescription = [mxHandler displayTextForEvent:redactedEvent withRoomState:nil inSubtitleMode:YES];
|
||||
if (!_lastEventDescription.length) {
|
||||
// The current last event must be removed, decrement the unread count (if not null)
|
||||
if (_unreadCount) {
|
||||
_unreadCount--;
|
||||
|
||||
if (_unreadCount == 0) {
|
||||
_containsBingUnread = NO;
|
||||
} // else _containsBingUnread may be false, we should perhaps reset this flag here
|
||||
}
|
||||
// Trigger back pagination to get an event with a non empty description
|
||||
[self triggerBackPagination];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)resetUnreadCount {
|
||||
_unreadCount = 0;
|
||||
_containsBingUnread = NO;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self cancelBackPagination];
|
||||
lastEvent = nil;
|
||||
_lastEventDescription = nil;
|
||||
}
|
||||
|
||||
- (void)triggerBackPagination {
|
||||
// Add listener if it is not already done
|
||||
if (!backPaginationListener) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
mxRoom = [mxHandler.mxSession roomWithRoomId:_roomId];
|
||||
if (mxRoom) {
|
||||
backPaginationListener = [mxRoom listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
|
||||
// Handle only backward events (Sanity check: be sure that the description has not been set by an other way)
|
||||
if (direction == MXEventDirectionBackwards && !_lastEventDescription.length) {
|
||||
if ([self updateWithLastEvent:event andRoomState:roomState markAsUnread:NO]) {
|
||||
// Force recents refresh
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kRecentRoomUpdatedByBackPagination object:_roomId];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
// Trigger a back pagination by reseting first backState to get room history from live
|
||||
[mxRoom resetBackState];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mxRoom.canPaginate) {
|
||||
backPaginationOperation = [mxRoom paginateBackMessages:10 complete:^{
|
||||
backPaginationOperation = nil;
|
||||
// Check whether another back pagination is required
|
||||
if (!_lastEventDescription.length) {
|
||||
[self triggerBackPagination];
|
||||
}
|
||||
} failure:^(NSError *error) {
|
||||
backPaginationOperation = nil;
|
||||
NSLog(@"[RecentRoom] Failed to paginate back: %@", error);
|
||||
[self cancelBackPagination];
|
||||
}];
|
||||
} else {
|
||||
// Force recents refresh
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kRecentRoomUpdatedByBackPagination object:_roomId];
|
||||
[self cancelBackPagination];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelBackPagination {
|
||||
if (backPaginationListener && mxRoom) {
|
||||
[mxRoom removeListener:backPaginationListener];
|
||||
backPaginationListener = nil;
|
||||
mxRoom = nil;
|
||||
}
|
||||
if (backPaginationOperation) {
|
||||
[backPaginationOperation cancel];
|
||||
backPaginationOperation = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "RoomMessageComponent.h"
|
||||
|
||||
#define ROOM_MESSAGE_DEFAULT_MAX_TEXTVIEW_WIDTH 200
|
||||
#define ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH 192
|
||||
#define ROOM_MESSAGE_TEXTVIEW_MARGIN 5
|
||||
|
||||
extern NSString *const kRoomMessageLocalPreviewKey;
|
||||
extern NSString *const kRoomMessageUploadIdKey;
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
// Text type
|
||||
RoomMessageTypeText,
|
||||
// Attachment type
|
||||
RoomMessageTypeImage,
|
||||
RoomMessageTypeAudio,
|
||||
RoomMessageTypeVideo,
|
||||
RoomMessageTypeLocation
|
||||
} RoomMessageType;
|
||||
|
||||
// Converts matrix events in room messages
|
||||
@interface RoomMessage : NSObject
|
||||
|
||||
@property (nonatomic) RoomMessageType messageType;
|
||||
@property (nonatomic) NSString *senderId;
|
||||
@property (nonatomic) NSString *senderName;
|
||||
@property (nonatomic) NSString *senderAvatarUrl;
|
||||
|
||||
// The max width of the text view used to display the text message (relevant only when type = RoomMessageTypeText)
|
||||
@property (nonatomic) CGFloat maxTextViewWidth;
|
||||
|
||||
// The message content size depends on its type:
|
||||
// - Text (RoomMessageTypeText): returns suitable content size of a text view to display the whole text message (respecting maxTextViewWidth)
|
||||
// - Attachment: returns suitable content size for an image view in order to display attachment thumbnail or icon.
|
||||
@property (nonatomic) CGSize contentSize;
|
||||
// Returns message components (Note: only one component is supported for attachment [messageType != RoomMessageTypeText])
|
||||
@property (nonatomic) NSArray *components;
|
||||
|
||||
// The body of the message, or kind of content description in case of attachment (e.g. "image attachment")
|
||||
@property (nonatomic) NSAttributedString *attributedTextMessage;
|
||||
// True if the sender name appears at the beginning of the message text (available only for messageType is RoomMessageTypeText)
|
||||
@property (nonatomic) BOOL startsWithSenderName;
|
||||
|
||||
// Attachment info (nil when messageType is RoomMessageTypeText)
|
||||
@property (nonatomic) BOOL isUploadInProgress;
|
||||
@property (nonatomic) NSString *attachmentURL;
|
||||
@property (nonatomic) NSDictionary *attachmentInfo;
|
||||
@property (nonatomic) NSString *thumbnailURL;
|
||||
@property (nonatomic) NSDictionary *thumbnailInfo;
|
||||
@property (nonatomic) UIImageOrientation thumbnailOrientation;
|
||||
@property (nonatomic) NSString *previewURL;
|
||||
@property (nonatomic) NSString *uploadId;
|
||||
@property (nonatomic) CGFloat uploadProgress;
|
||||
|
||||
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState;
|
||||
|
||||
// Concatenates successive text messages from the same user
|
||||
// Return false if the provided event could not be added (for example the sender id is not the same, the sender name has been changed, or the messageType is not RoomMessageTypeText)
|
||||
- (BOOL)addEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState;
|
||||
// Search a component with the local id, and update it with the provided id
|
||||
// Return false if the local event id is not found
|
||||
- (BOOL)replaceLocalEventId:(NSString *)localEventId withEventId:(NSString *)eventId;
|
||||
// Remove the item defined with this event id
|
||||
// Return false if the event id is not found
|
||||
- (BOOL)removeEvent:(NSString*)eventId;
|
||||
// Looks for the item defined with the same id than the provided event, then updates it with the redacted event.
|
||||
// Return false if the event id is not found
|
||||
- (BOOL)updateRedactedEvent:(MXEvent*)redactedEvent;
|
||||
|
||||
// Returns the component from the eventId
|
||||
- (RoomMessageComponent*)componentWithEventId:(NSString *)eventId;
|
||||
// Return true if the event id is one of the message items
|
||||
- (BOOL)containsEventId:(NSString*)eventId;
|
||||
|
||||
// Return true if the provided message has the same sender as the receiver (same sender means here same id, same name and same avatar)
|
||||
- (BOOL)hasSameSenderAsRoomMessage:(RoomMessage*)roomMessage;
|
||||
|
||||
// Add component(s) of the provided message to the receiver, return true on success (failed if one of the message type is not RoomMessageTypeText)
|
||||
- (BOOL)mergeWithRoomMessage:(RoomMessage*)roomMessage;
|
||||
|
||||
// Compute height of each component if they are not already available (relevant only when type = RoomMessageTypeText)
|
||||
- (void)checkComponentsHeight;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,637 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "RoomMessage.h"
|
||||
|
||||
#import "MatrixSDKHandler.h"
|
||||
#import "AppSettings.h"
|
||||
#import "MXCTools.h"
|
||||
|
||||
NSString *const kRoomMessageLocalPreviewKey = @"kRoomMessageLocalPreviewKey";
|
||||
NSString *const kRoomMessageUploadIdKey = @"kRoomMessageUploadIdKey";
|
||||
|
||||
static NSAttributedString *messageSeparator = nil;
|
||||
|
||||
@interface RoomMessage() {
|
||||
// Array of RoomMessageComponent
|
||||
NSMutableArray *messageComponents;
|
||||
// Current text message reset at each component change (see attributedTextMessage property)
|
||||
NSMutableAttributedString *currentAttributedTextMsg;
|
||||
|
||||
BOOL shouldUpdateComponentsHeight;
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)messageSeparator;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RoomMessage
|
||||
|
||||
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState {
|
||||
if (self = [super init]) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
_senderId = event.userId;
|
||||
_senderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
|
||||
_senderAvatarUrl = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
|
||||
_maxTextViewWidth = ROOM_MESSAGE_DEFAULT_MAX_TEXTVIEW_WIDTH;
|
||||
_contentSize = CGSizeZero;
|
||||
_uploadProgress = -1;
|
||||
currentAttributedTextMsg = nil;
|
||||
|
||||
// Set message type (consider text by default), and check attachment if any
|
||||
_messageType = RoomMessageTypeText;
|
||||
if ([mxHandler isSupportedAttachment:event]) {
|
||||
// Note: event.eventType is equal here to MXEventTypeRoomMessage
|
||||
|
||||
// Set default thumbnail orientation
|
||||
_thumbnailOrientation = UIImageOrientationUp;
|
||||
|
||||
NSString *msgtype = event.content[@"msgtype"];
|
||||
if ([msgtype isEqualToString:kMXMessageTypeImage]) {
|
||||
_messageType = RoomMessageTypeImage;
|
||||
// Retrieve content url/info
|
||||
_attachmentURL = event.content[@"url"];
|
||||
_attachmentInfo = event.content[@"info"];
|
||||
// Handle legacy thumbnail url/info (Not defined anymore in recent attachments)
|
||||
_thumbnailURL = event.content[@"thumbnail_url"];
|
||||
_thumbnailInfo = event.content[@"thumbnail_info"];
|
||||
if (!_thumbnailURL) {
|
||||
// Suppose _attachmentURL is a matrix content uri, we use SDK to get the well adapted thumbnail from server
|
||||
_thumbnailURL = [mxHandler thumbnailURLForContent:_attachmentURL inViewSize:self.contentSize withMethod:MXThumbnailingMethodScale];
|
||||
|
||||
// Check whether the image has been uploaded with an orientation
|
||||
if (_attachmentInfo[@"rotation"]) {
|
||||
// Currently the matrix content server provides thumbnails by ignoring the original image orientation.
|
||||
// We store here the actual orientation to apply it on downloaded thumbnail.
|
||||
_thumbnailOrientation = [MXCTools imageOrientationForRotationAngleInDegree:[_attachmentInfo[@"rotation"] integerValue]];
|
||||
|
||||
// Rotate the current content size (if need)
|
||||
if (_thumbnailOrientation == UIImageOrientationLeft || _thumbnailOrientation == UIImageOrientationRight) {
|
||||
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ([msgtype isEqualToString:kMXMessageTypeAudio]) {
|
||||
// Not supported yet
|
||||
//_messageType = RoomMessageTypeAudio;
|
||||
} else if ([msgtype isEqualToString:kMXMessageTypeVideo]) {
|
||||
_messageType = RoomMessageTypeVideo;
|
||||
// Retrieve content url/info
|
||||
_attachmentURL = event.content[@"url"];
|
||||
_attachmentInfo = event.content[@"info"];
|
||||
if (_attachmentInfo) {
|
||||
// Get video thumbnail info
|
||||
_thumbnailURL = _attachmentInfo[@"thumbnail_url"];
|
||||
_thumbnailInfo = _attachmentInfo[@"thumbnail_info"];
|
||||
}
|
||||
} else if ([msgtype isEqualToString:kMXMessageTypeLocation]) {
|
||||
// Not supported yet
|
||||
// _messageType = RoomMessageTypeLocation;
|
||||
}
|
||||
// Retrieve local preview url (if any)
|
||||
_previewURL = event.content[kRoomMessageLocalPreviewKey];
|
||||
// Retrieve upload id (if any)
|
||||
_uploadId = event.content[kRoomMessageUploadIdKey];
|
||||
}
|
||||
|
||||
// Set first component of the current message
|
||||
RoomMessageComponent *msgComponent = [[RoomMessageComponent alloc] initWithEvent:event andRoomState:roomState];
|
||||
if (msgComponent) {
|
||||
messageComponents = [NSMutableArray array];
|
||||
[messageComponents addObject:msgComponent];
|
||||
if (_messageType == RoomMessageTypeText) {
|
||||
// Set text range
|
||||
msgComponent.range = NSMakeRange(0, msgComponent.textMessage.length);
|
||||
|
||||
// Compute the height of the text component
|
||||
msgComponent.height = [self rawTextHeight:self.attributedTextMessage];
|
||||
shouldUpdateComponentsHeight = NO;
|
||||
}
|
||||
} else {
|
||||
// Ignore this event
|
||||
self = nil;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
messageComponents = nil;
|
||||
}
|
||||
|
||||
- (BOOL)addEvent:(MXEvent *)event withRoomState:(MXRoomState*)roomState {
|
||||
// We group together text messages from the same user
|
||||
if ([event.userId isEqualToString:_senderId] && (_messageType == RoomMessageTypeText)) {
|
||||
// Attachments (image, video ...) cannot be added here
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
if ([mxHandler isSupportedAttachment:event]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Check sender information
|
||||
NSString *eventSenderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
|
||||
NSString *eventSenderAvatar = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
|
||||
if ((_senderName || eventSenderName) &&
|
||||
([_senderName isEqualToString:eventSenderName] == NO)) {
|
||||
return NO;
|
||||
}
|
||||
if ((_senderAvatarUrl || eventSenderAvatar) &&
|
||||
([_senderAvatarUrl isEqualToString:eventSenderAvatar] == NO)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Create new message component
|
||||
RoomMessageComponent *addedComponent = [[RoomMessageComponent alloc] initWithEvent:event andRoomState:roomState];
|
||||
if (addedComponent) {
|
||||
[self addComponent:addedComponent];
|
||||
}
|
||||
// else the event is ignored, we consider it as handled
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)replaceLocalEventId:(NSString *)localEventId withEventId:(NSString *)eventId {
|
||||
NSUInteger index = messageComponents.count;
|
||||
while (index--) {
|
||||
RoomMessageComponent* msgComponent = [messageComponents objectAtIndex:index];
|
||||
if ([msgComponent.eventId isEqualToString:localEventId]) {
|
||||
msgComponent.eventId = eventId;
|
||||
// Refresh global attributed string (if any) to take into account potential component style change
|
||||
if (currentAttributedTextMsg) {
|
||||
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
|
||||
[currentAttributedTextMsg replaceCharactersInRange:msgComponent.range withAttributedString:attributedString];
|
||||
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
} // else let the getter "attributedTextMessage" build it
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
// here the provided eventId has not been found
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)removeEvent:(NSString *)eventId {
|
||||
if (_messageType == RoomMessageTypeText) {
|
||||
NSUInteger index = messageComponents.count;
|
||||
while (index--) {
|
||||
RoomMessageComponent* msgComponent = [messageComponents objectAtIndex:index];
|
||||
if ([msgComponent.eventId isEqualToString:eventId]) {
|
||||
[messageComponents removeObjectAtIndex:index];
|
||||
|
||||
// Refresh global attributed string (if any)
|
||||
if (currentAttributedTextMsg) {
|
||||
if (!messageComponents.count) {
|
||||
// The message is now empty - Reset
|
||||
self.attributedTextMessage = nil;
|
||||
} else {
|
||||
// Update
|
||||
[currentAttributedTextMsg deleteCharactersInRange:msgComponent.range];
|
||||
// Remove a separator
|
||||
NSRange separatorRange = NSMakeRange(0, [RoomMessage messageSeparator].length);
|
||||
if (msgComponent.range.location) {
|
||||
separatorRange.location = msgComponent.range.location - [RoomMessage messageSeparator].length;
|
||||
}
|
||||
[currentAttributedTextMsg deleteCharactersInRange:separatorRange];
|
||||
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
}
|
||||
} // else let the getter "attributedTextMessage" build it
|
||||
|
||||
// Adjust range for components displayed after this removed component
|
||||
NSUInteger deletedLength = msgComponent.range.length + [RoomMessage messageSeparator].length;
|
||||
for (; index < messageComponents.count; index++) {
|
||||
msgComponent = [messageComponents objectAtIndex:index];
|
||||
NSRange range = msgComponent.range;
|
||||
NSAssert(range.location >= deletedLength, @"RoomMessage: the ranges of msg components are corrupted");
|
||||
if (range.location >= deletedLength) {
|
||||
range.location -= deletedLength;
|
||||
} else {
|
||||
range.location = 0;
|
||||
}
|
||||
msgComponent.range = range;
|
||||
}
|
||||
|
||||
// Height of each components should be updated
|
||||
shouldUpdateComponentsHeight = YES;
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
// here the provided eventId has not been found
|
||||
} else {
|
||||
// Consider here message with no more than one element
|
||||
if (messageComponents.count) {
|
||||
RoomMessageComponent *msgComponent = [messageComponents firstObject];
|
||||
if ([msgComponent.eventId isEqualToString:eventId]) {
|
||||
[messageComponents removeObjectAtIndex:0];
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)updateRedactedEvent:(MXEvent*)redactedEvent {
|
||||
// Check whether the provided event is a redacted one
|
||||
if (!redactedEvent.redactedBecause) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_messageType == RoomMessageTypeText) {
|
||||
NSUInteger index = messageComponents.count;
|
||||
while (index--) {
|
||||
RoomMessageComponent* msgComponent = [messageComponents objectAtIndex:index];
|
||||
if ([msgComponent.eventId isEqualToString:redactedEvent.eventId]) {
|
||||
// Update component with redacted event, remove it if the resulting string is empty
|
||||
[msgComponent updateWithRedactedEvent:redactedEvent];
|
||||
if (!msgComponent.textMessage.length) {
|
||||
[self removeEvent:redactedEvent.eventId];
|
||||
} else {
|
||||
// Compute the SIGNED difference of length (old length - new length)
|
||||
NSInteger diffLength = msgComponent.range.length - msgComponent.textMessage.length;
|
||||
|
||||
// Refresh global attributed string (if any)
|
||||
if (currentAttributedTextMsg) {
|
||||
// Replace the component string
|
||||
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
|
||||
[currentAttributedTextMsg replaceCharactersInRange:msgComponent.range withAttributedString:attributedString];
|
||||
|
||||
// Update the component range
|
||||
NSRange updatedRange = msgComponent.range;
|
||||
updatedRange.length = msgComponent.textMessage.length;
|
||||
msgComponent.range = updatedRange;
|
||||
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
} // else let the getter "attributedTextMessage" build it
|
||||
|
||||
// Adjust range for components displayed after this updated component
|
||||
for (index++; index < messageComponents.count; index++) {
|
||||
msgComponent = [messageComponents objectAtIndex:index];
|
||||
NSRange range = msgComponent.range;
|
||||
NSAssert((diffLength < 0) || (range.location >= diffLength), @"RoomMessage: the ranges of msg components are corrupted");
|
||||
range.location -= diffLength;
|
||||
msgComponent.range = range;
|
||||
}
|
||||
|
||||
// Height of each components should be updated
|
||||
shouldUpdateComponentsHeight = YES;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Consider here message related to attachment (This message has no more than one element)
|
||||
if (messageComponents.count) {
|
||||
RoomMessageComponent *msgComponent = [messageComponents firstObject];
|
||||
if ([msgComponent.eventId isEqualToString:redactedEvent.eventId]) {
|
||||
// Redaction removes the attachment information, the message becomes a text message
|
||||
_messageType = RoomMessageTypeText;
|
||||
_attachmentURL = nil;
|
||||
_attachmentInfo = nil;
|
||||
_thumbnailURL = nil;
|
||||
_thumbnailInfo = nil;
|
||||
_previewURL = nil;
|
||||
_uploadId = nil;
|
||||
_uploadProgress = -1;
|
||||
|
||||
[msgComponent updateWithRedactedEvent:redactedEvent];
|
||||
if (!msgComponent.textMessage.length) {
|
||||
[self removeEvent:redactedEvent.eventId];
|
||||
} else {
|
||||
// Set text range
|
||||
msgComponent.range = NSMakeRange(0, msgComponent.textMessage.length);
|
||||
|
||||
// Compute the height of the text component
|
||||
msgComponent.height = [self rawTextHeight:self.attributedTextMessage];
|
||||
shouldUpdateComponentsHeight = NO;
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (RoomMessageComponent*)componentWithEventId:(NSString *)eventId {
|
||||
for (RoomMessageComponent* msgComponent in messageComponents) {
|
||||
if ([msgComponent.eventId isEqualToString:eventId]) {
|
||||
return msgComponent;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)containsEventId:(NSString *)eventId {
|
||||
return nil != [self componentWithEventId:eventId];
|
||||
}
|
||||
|
||||
- (BOOL)hasSameSenderAsRoomMessage:(RoomMessage*)roomMessage {
|
||||
// NOTE: same sender means here same id, same name and same avatar
|
||||
// Check first user id
|
||||
if ([_senderId isEqualToString:roomMessage.senderId] == NO) {
|
||||
return NO;
|
||||
}
|
||||
// Check sender name
|
||||
if ((_senderName.length || roomMessage.senderName.length) && ([_senderName isEqualToString:roomMessage.senderName] == NO)) {
|
||||
return NO;
|
||||
}
|
||||
// Check avatar url
|
||||
if ((_senderAvatarUrl.length || roomMessage.senderAvatarUrl.length) && ([_senderAvatarUrl isEqualToString:roomMessage.senderAvatarUrl] == NO)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)mergeWithRoomMessage:(RoomMessage*)roomMessage {
|
||||
if ([self hasSameSenderAsRoomMessage:roomMessage]) {
|
||||
if ((_messageType == RoomMessageTypeText) && (roomMessage.messageType == RoomMessageTypeText)) {
|
||||
// Add all components of the provided message
|
||||
for (RoomMessageComponent* msgComponent in roomMessage.components) {
|
||||
[self addComponent:msgComponent];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)checkComponentsHeight {
|
||||
// Check conditions to ignore this action
|
||||
if (_messageType != RoomMessageTypeText || !shouldUpdateComponentsHeight || !messageComponents.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This method should run on main thread
|
||||
if ([NSThread currentThread] != [NSThread mainThread]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self checkComponentsHeight];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute height of the first component
|
||||
RoomMessageComponent *msgComponent = [messageComponents objectAtIndex:0];
|
||||
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
|
||||
CGSize textContentSize = [self textContentSize:attributedString];
|
||||
if (textContentSize.height) {
|
||||
msgComponent.height = textContentSize.height - (2 * ROOM_MESSAGE_TEXTVIEW_MARGIN);
|
||||
} else {
|
||||
msgComponent.height = 0;
|
||||
}
|
||||
// Compute height of other components
|
||||
for (NSUInteger index = 1; index < messageComponents.count; index++) {
|
||||
CGFloat previousContentHeight = textContentSize.height ? textContentSize.height : (2 * ROOM_MESSAGE_TEXTVIEW_MARGIN);
|
||||
msgComponent = [messageComponents objectAtIndex:index];
|
||||
// Append attributed text
|
||||
[attributedString appendAttributedString:[RoomMessage messageSeparator]];
|
||||
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]]];
|
||||
textContentSize = [self textContentSize:attributedString];
|
||||
msgComponent.height = textContentSize.height - previousContentHeight;
|
||||
}
|
||||
shouldUpdateComponentsHeight = NO;
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setMaxTextViewWidth:(CGFloat)maxTextViewWidth {
|
||||
if (_messageType == RoomMessageTypeText) {
|
||||
// Check change
|
||||
if (_maxTextViewWidth != maxTextViewWidth) {
|
||||
_maxTextViewWidth = maxTextViewWidth;
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
// Invalidate existing height for components
|
||||
shouldUpdateComponentsHeight = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)contentSize {
|
||||
if (CGSizeEqualToSize(_contentSize, CGSizeZero)) {
|
||||
if (_messageType == RoomMessageTypeText) {
|
||||
if ([NSThread currentThread] != [NSThread mainThread]) {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
_contentSize = [self textContentSize:self.attributedTextMessage];
|
||||
});
|
||||
} else {
|
||||
_contentSize = [self textContentSize:self.attributedTextMessage];
|
||||
}
|
||||
} else if (_messageType == RoomMessageTypeImage || _messageType == RoomMessageTypeVideo) {
|
||||
CGFloat width, height;
|
||||
width = height = 40;
|
||||
if (_thumbnailInfo || _attachmentInfo) {
|
||||
if (_thumbnailInfo && _thumbnailInfo[@"w"] && _thumbnailInfo[@"h"]) {
|
||||
width = [_thumbnailInfo[@"w"] integerValue];
|
||||
height = [_thumbnailInfo[@"h"] integerValue];
|
||||
} else if (_attachmentInfo[@"w"] && _attachmentInfo[@"h"]) {
|
||||
width = [_attachmentInfo[@"w"] integerValue];
|
||||
height = [_attachmentInfo[@"h"] integerValue];
|
||||
}
|
||||
|
||||
if (width > ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH || height > ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH) {
|
||||
if (width > height) {
|
||||
height = (height * ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH) / width;
|
||||
height = floorf(height / 2) * 2;
|
||||
width = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
|
||||
} else {
|
||||
width = (width * ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH) / height;
|
||||
width = floorf(width / 2) * 2;
|
||||
height = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check here thumbnail orientation
|
||||
if (_thumbnailOrientation == UIImageOrientationLeft || _thumbnailOrientation == UIImageOrientationRight) {
|
||||
_contentSize = CGSizeMake(height, width);
|
||||
} else {
|
||||
_contentSize = CGSizeMake(width, height);
|
||||
}
|
||||
} else {
|
||||
_contentSize = CGSizeMake(40, 40);
|
||||
}
|
||||
}
|
||||
return _contentSize;
|
||||
}
|
||||
|
||||
- (NSArray*)components {
|
||||
return [messageComponents copy];
|
||||
}
|
||||
|
||||
- (void)setAttributedTextMessage:(NSAttributedString *)inAttributedTextMessage {
|
||||
if (!inAttributedTextMessage.length) {
|
||||
currentAttributedTextMsg = nil;
|
||||
} else {
|
||||
currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:inAttributedTextMessage];
|
||||
}
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
}
|
||||
|
||||
- (NSAttributedString*)attributedTextMessage {
|
||||
if (!currentAttributedTextMsg && messageComponents.count) {
|
||||
// Create attributed string
|
||||
for (RoomMessageComponent* msgComponent in messageComponents) {
|
||||
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
|
||||
if (!currentAttributedTextMsg) {
|
||||
currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
|
||||
} else {
|
||||
// Append attributed text
|
||||
[currentAttributedTextMsg appendAttributedString:[RoomMessage messageSeparator]];
|
||||
[currentAttributedTextMsg appendAttributedString:attributedString];
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentAttributedTextMsg;
|
||||
}
|
||||
|
||||
- (BOOL)startsWithSenderName {
|
||||
if (_messageType == RoomMessageTypeText) {
|
||||
if (messageComponents.count) {
|
||||
RoomMessageComponent *msgComponent = [messageComponents objectAtIndex:0];
|
||||
return msgComponent.startsWithSenderName;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isUploadInProgress {
|
||||
if (_messageType != RoomMessageTypeText) {
|
||||
if (messageComponents.count) {
|
||||
RoomMessageComponent *msgComponent = [messageComponents firstObject];
|
||||
return (msgComponent.style == RoomMessageComponentStyleInProgress);
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Privates
|
||||
|
||||
- (void)addComponent:(RoomMessageComponent*)addedComponent {
|
||||
// Check date of existing components to insert this new one
|
||||
NSUInteger addedTextLength = addedComponent.textMessage.length;
|
||||
NSUInteger addedTextLocation = 0;
|
||||
NSUInteger separatorLocation = 0;
|
||||
NSUInteger index = messageComponents.count;
|
||||
while (index) {
|
||||
RoomMessageComponent *msgComponent = [messageComponents objectAtIndex:(--index)];
|
||||
if ([msgComponent.date compare:addedComponent.date] == NSOrderedDescending) {
|
||||
// New component will be inserted before this one -> Adjust text range
|
||||
NSRange range = msgComponent.range;
|
||||
range.location += (addedTextLength + [RoomMessage messageSeparator].length);
|
||||
msgComponent.range = range;
|
||||
} else {
|
||||
// New component will be inserted here
|
||||
index ++;
|
||||
addedTextLocation = msgComponent.range.location + msgComponent.range.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Insert new component
|
||||
[messageComponents insertObject:addedComponent atIndex:index];
|
||||
|
||||
// Adjust added component location
|
||||
if (addedTextLocation) {
|
||||
// We will insert separator before adding new component
|
||||
separatorLocation = addedTextLocation;
|
||||
addedTextLocation += [RoomMessage messageSeparator].length;
|
||||
} else {
|
||||
// The new component is inserted in first position, separator will be inserted after it
|
||||
separatorLocation = addedTextLength;
|
||||
}
|
||||
|
||||
// Update global attributed string (if any)
|
||||
if (currentAttributedTextMsg) {
|
||||
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:addedComponent.textMessage attributes:[addedComponent stringAttributes]];
|
||||
if (addedTextLocation) {
|
||||
// Add separator before added text component
|
||||
[currentAttributedTextMsg insertAttributedString:[RoomMessage messageSeparator] atIndex:separatorLocation];
|
||||
[currentAttributedTextMsg insertAttributedString:attributedString atIndex:addedTextLocation];
|
||||
} else {
|
||||
// The new component is inserted in first position
|
||||
[currentAttributedTextMsg insertAttributedString:attributedString atIndex:addedTextLocation];
|
||||
// Check whether a separator is required
|
||||
if (messageComponents.count > 1) {
|
||||
[currentAttributedTextMsg insertAttributedString:[RoomMessage messageSeparator] atIndex:separatorLocation];
|
||||
}
|
||||
}
|
||||
|
||||
// Reset content size
|
||||
_contentSize = CGSizeZero;
|
||||
|
||||
} // Else let the getter "attributedTextMessage" build it
|
||||
|
||||
// Set text range
|
||||
addedComponent.range = NSMakeRange(addedTextLocation, addedTextLength);
|
||||
|
||||
// Height of each components should be computed again
|
||||
shouldUpdateComponentsHeight = YES;
|
||||
}
|
||||
|
||||
#pragma mark - Text measuring
|
||||
|
||||
// Return the raw height of the provided text by removing any margin
|
||||
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText {
|
||||
__block CGSize textSize;
|
||||
if ([NSThread currentThread] != [NSThread mainThread]) {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
textSize = [self textContentSize:attributedText];
|
||||
});
|
||||
} else {
|
||||
textSize = [self textContentSize:attributedText];
|
||||
}
|
||||
|
||||
if (textSize.height) {
|
||||
// Return the actual height of the text by removing textview margin from content height
|
||||
return (textSize.height - (2 * ROOM_MESSAGE_TEXTVIEW_MARGIN));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Return the content size of a text view initialized with the provided attributed text
|
||||
// CAUTION: This method runs only on main thread
|
||||
- (CGSize)textContentSize: (NSAttributedString*)attributedText {
|
||||
if (attributedText.length) {
|
||||
// Use a TextView template
|
||||
UITextView *dummyTextView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, _maxTextViewWidth, MAXFLOAT)];
|
||||
dummyTextView.attributedText = attributedText;
|
||||
return [dummyTextView sizeThatFits:dummyTextView.frame.size];
|
||||
}
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (NSAttributedString *)messageSeparator {
|
||||
@synchronized(self) {
|
||||
if(messageSeparator == nil) {
|
||||
messageSeparator = [[NSAttributedString alloc] initWithString:@"\r\n\r\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor],
|
||||
NSFontAttributeName: [UIFont systemFontOfSize:4]}];
|
||||
}
|
||||
}
|
||||
return messageSeparator;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 <MatrixSDK/MatrixSDK.h>
|
||||
|
||||
extern NSString *const kLocalEchoEventIdPrefix;
|
||||
extern NSString *const kFailedEventIdPrefix;
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
RoomMessageComponentStyleDefault,
|
||||
RoomMessageComponentStyleBing,
|
||||
RoomMessageComponentStyleInProgress,
|
||||
RoomMessageComponentStyleFailed,
|
||||
RoomMessageComponentStyleUnsupported
|
||||
} RoomMessageComponentStyle;
|
||||
|
||||
@interface RoomMessageComponent : NSObject
|
||||
|
||||
@property (nonatomic) NSString *textMessage;
|
||||
@property (nonatomic) NSString *eventId;
|
||||
@property (nonatomic) NSDate *date;
|
||||
@property (nonatomic) RoomMessageComponentStyle style;
|
||||
@property (nonatomic) BOOL isStateEvent;
|
||||
@property (nonatomic) BOOL isRedactedEvent;
|
||||
@property (nonatomic) BOOL isIncomingMsg;
|
||||
|
||||
// Keep a reference on related event (used in case of redaction)
|
||||
@property (nonatomic, readonly) MXEvent *event;
|
||||
|
||||
// The following properties are defined to store information on component.
|
||||
// They must be handled by the object which creates the RoomMessageComponent instance.
|
||||
@property (nonatomic) CGFloat height;
|
||||
@property (nonatomic) NSRange range;
|
||||
|
||||
// True if text message starts with the sender name (see membership events, emote ...)
|
||||
@property (nonatomic) BOOL startsWithSenderName;
|
||||
|
||||
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState;
|
||||
- (NSDictionary *)stringAttributes;
|
||||
|
||||
- (void)updateWithRedactedEvent:(MXEvent*)redactedEvent;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "RoomMessageComponent.h"
|
||||
#import "MatrixSDKHandler.h"
|
||||
|
||||
NSString *const kLocalEchoEventIdPrefix = @"localEcho-";
|
||||
NSString *const kFailedEventIdPrefix = @"failedEventId-";
|
||||
|
||||
@implementation RoomMessageComponent
|
||||
|
||||
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState {
|
||||
if (self = [super init]) {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
// Build text component related to this event
|
||||
NSString* textMessage = [mxHandler displayTextForEvent:event withRoomState:roomState inSubtitleMode:NO];
|
||||
if (textMessage) {
|
||||
_textMessage = textMessage;
|
||||
|
||||
NSString *senderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
|
||||
_startsWithSenderName = ([textMessage hasPrefix:senderName] || [mxHandler isEmote:event]);
|
||||
|
||||
// Set date time text label
|
||||
if (event.originServerTs != kMXUndefinedTimestamp) {
|
||||
_date = [NSDate dateWithTimeIntervalSince1970:(double)event.originServerTs/1000];
|
||||
} else {
|
||||
_date = nil;
|
||||
}
|
||||
|
||||
// Set component flags
|
||||
_isStateEvent = event.isState;
|
||||
_isIncomingMsg = ([event.userId isEqualToString:mxHandler.userId] == NO);
|
||||
_isRedactedEvent = (event.redactedBecause != nil);
|
||||
|
||||
// Set eventId -> set the component style
|
||||
self.eventId = event.eventId;
|
||||
// Keep ref on event (used in case of redaction)
|
||||
_event = event;
|
||||
} else {
|
||||
// Ignore this event
|
||||
self = nil;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateWithRedactedEvent:(MXEvent*)redactedEvent {
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
|
||||
// Build text component related to this event (Note: we don't have valid room state here, userId will be used as display name)
|
||||
_textMessage = [mxHandler displayTextForEvent:redactedEvent withRoomState:nil inSubtitleMode:NO];
|
||||
_isRedactedEvent = YES;
|
||||
_event = redactedEvent;
|
||||
}
|
||||
|
||||
- (void)setEventId:(NSString *)eventId {
|
||||
_eventId = eventId;
|
||||
|
||||
// Update component style
|
||||
MatrixSDKHandler *mxHandler = [MatrixSDKHandler sharedHandler];
|
||||
if ([_textMessage hasPrefix:kMatrixSDKHandlerUnsupportedEventDescriptionPrefix]) {
|
||||
_style = RoomMessageComponentStyleUnsupported;
|
||||
} else if ([_eventId hasPrefix:kFailedEventIdPrefix]) {
|
||||
_style = RoomMessageComponentStyleFailed;
|
||||
} else if (_isIncomingMsg && !_isStateEvent && !_isRedactedEvent && [mxHandler containsBingWord:_textMessage]) {
|
||||
_style = RoomMessageComponentStyleBing;
|
||||
} else if (!_isIncomingMsg && [_eventId hasPrefix:kLocalEchoEventIdPrefix]) {
|
||||
_style = RoomMessageComponentStyleInProgress;
|
||||
} else {
|
||||
_style = RoomMessageComponentStyleDefault;
|
||||
}
|
||||
|
||||
// Update stored event id (Note: Only local event Ids are supposed to change)
|
||||
_event.eventId = eventId;
|
||||
}
|
||||
|
||||
- (NSDictionary*)stringAttributes {
|
||||
UIColor *textColor;
|
||||
UIFont *font;
|
||||
|
||||
switch (_style) {
|
||||
case RoomMessageComponentStyleDefault:
|
||||
textColor = [UIColor blackColor];
|
||||
break;
|
||||
case RoomMessageComponentStyleBing:
|
||||
textColor = [UIColor blueColor];
|
||||
break;
|
||||
case RoomMessageComponentStyleInProgress:
|
||||
textColor = [UIColor lightGrayColor];
|
||||
break;
|
||||
case RoomMessageComponentStyleFailed:
|
||||
case RoomMessageComponentStyleUnsupported:
|
||||
textColor = [UIColor redColor];
|
||||
break;
|
||||
default:
|
||||
textColor = [UIColor blackColor];
|
||||
break;
|
||||
}
|
||||
|
||||
if (_isStateEvent) {
|
||||
font = [UIFont italicSystemFontOfSize:14];
|
||||
} else {
|
||||
font = [UIFont systemFontOfSize:14];
|
||||
}
|
||||
|
||||
return @{
|
||||
NSForegroundColorAttributeName : textColor,
|
||||
NSFontAttributeName: font
|
||||
};
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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/Foundation.h>
|
||||
|
||||
@interface SectionedContacts : NSObject {
|
||||
int contactsCount;
|
||||
NSArray *sectionTitles;
|
||||
NSArray *sectionedContacts;
|
||||
}
|
||||
|
||||
@property (nonatomic, readonly) int contactsCount;
|
||||
@property (nonatomic, readonly) NSArray *sectionTitles;
|
||||
@property (nonatomic, readonly) NSArray *sectionedContacts;
|
||||
|
||||
-(id)initWithContacts:(NSArray *)_sectionedContacts andTitles:(NSArray *)titles andCount:(int)count;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2014 OpenMarket 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 "SectionedContacts.h"
|
||||
|
||||
@implementation SectionedContacts
|
||||
|
||||
@synthesize contactsCount, sectionTitles, sectionedContacts;
|
||||
|
||||
-(id)initWithContacts:(NSArray *)_sectionedContacts andTitles:(NSArray *)titles andCount:(int)count {
|
||||
if (self = [super init]) {
|
||||
contactsCount = count;
|
||||
sectionedContacts = _sectionedContacts;
|
||||
sectionTitles = titles;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user