Moved code under a subdirectory: Console

This commit is contained in:
manuroe
2015-04-03 09:04:22 +02:00
parent 1b24cdb7f1
commit 2b0350d14f
160 changed files with 0 additions and 0 deletions
+95
View File
@@ -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
+168
View File
@@ -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
+53
View File
@@ -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
+232
View File
@@ -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
+66
View File
@@ -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
+369
View File
@@ -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
+30
View File
@@ -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
+92
View File
@@ -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
+37
View File
@@ -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
+166
View File
@@ -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
+100
View File
@@ -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
+637
View File
@@ -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