Merge branch 'develop' into apns

This commit is contained in:
David Baker 2015-01-13 14:28:47 +00:00
commit 53e80c5706
51 changed files with 4658 additions and 1228 deletions

View file

@ -20,10 +20,10 @@ PODS:
- AFNetworking/UIKit (2.4.1):
- AFNetworking/NSURLConnection
- AFNetworking/NSURLSession
- Mantle (1.5.1):
- Mantle/extobjc (= 1.5.1)
- Mantle/extobjc (1.5.1)
- MatrixSDK (0.1.0):
- Mantle (1.5.3):
- Mantle/extobjc (= 1.5.3)
- Mantle/extobjc (1.5.3)
- MatrixSDK (0.2.0):
- AFNetworking (~> 2.4.1)
- Mantle (~> 1.5)
@ -36,7 +36,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AFNetworking: 0aabc6fae66d6e5d039eeb21c315843c7aae51ab
Mantle: d7c5ac734579ec751c58fecbf56189853056c58c
MatrixSDK: e65916e5bb4e327b36f05e52a2b9e4ec83398d58
Mantle: 8d84cacd6c2a69ff6fbce985a2b51298a5495de3
MatrixSDK: 74bdc315f4f3422b7142704a5bde193583fd0a56
COCOAPODS: 0.35.0

View file

@ -7,5 +7,5 @@ Before opening the sample workspace, you need to build it with the CocoaPods com
This will load your local SDK source code into the sample workspace.
Then, open ``syMessaging.xcworkspace``.

View file

@ -7,13 +7,19 @@
objects = {
/* Begin PBXBuildFile section */
71D2E4EC1A49814B000DE015 /* MemberActionsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D2E4EB1A49814B000DE015 /* MemberActionsCell.m */; };
71DB9DC11A495B6400504A09 /* MemberViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 71DB9DC01A495B6400504A09 /* MemberViewController.m */; };
71E94A771A5C4020009F52E5 /* PieChartView.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E94A761A5C4020009F52E5 /* PieChartView.m */; };
D648B86A591308736E2D4078 /* libPods-matrixConsole.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8141B1E2401FFCC3C5B99234 /* libPods-matrixConsole.a */; };
F00B5DB91A1B9BCE00EA1C8D /* CustomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = F00B5DB81A1B9BCE00EA1C8D /* CustomImageView.m */; };
F013EEEC1A40D437002BB093 /* matrixConsole-Defaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = F013EEEB1A40D437002BB093 /* matrixConsole-Defaults.plist */; };
F01628C119E29C660071C473 /* default-profile.png in Resources */ = {isa = PBXBuildFile; fileRef = F01628BC19E29C660071C473 /* default-profile.png */; };
F01628C319E29C660071C473 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = F01628BE19E29C660071C473 /* logo.png */; };
F01A0FF31A27314B009FAE2F /* RoomMessageComponent.m in Sources */ = {isa = PBXBuildFile; fileRef = F01A0FF21A27314B009FAE2F /* RoomMessageComponent.m */; };
F021FBEF1A5EF57300EA3AE6 /* MediaLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = F021FBEE1A5EF57300EA3AE6 /* MediaLoader.m */; };
F021FBF21A5F1F8E00EA3AE6 /* MediaManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F021FBF11A5F1F8E00EA3AE6 /* MediaManager.m */; };
F024098219E7D177006E741B /* tab_recents@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F024098119E7D177006E741B /* tab_recents@2x.png */; };
F02900BB1A63C71E00356F7D /* ConsoleTools.m in Sources */ = {isa = PBXBuildFile; fileRef = F02900BA1A63C71E00356F7D /* ConsoleTools.m */; };
F02BCE231A1A5A2B00543B47 /* play.png in Resources */ = {isa = PBXBuildFile; fileRef = F02BCE221A1A5A2B00543B47 /* play.png */; };
F02D707619F1DC9E007B47D3 /* RoomMemberTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F02D707519F1DC9E007B47D3 /* RoomMemberTableCell.m */; };
F03C47111A02952800E445AB /* CustomAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = F03C47101A02952800E445AB /* CustomAlert.m */; };
@ -24,7 +30,6 @@
F03EF5FA19F171EB00A0EE52 /* RoomViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5F319F171EB00A0EE52 /* RoomViewController.m */; };
F03EF5FB19F171EB00A0EE52 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5F519F171EB00A0EE52 /* SettingsViewController.m */; };
F03EF5FF19F1762000A0EE52 /* RoomMessageTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5FE19F1762000A0EE52 /* RoomMessageTableCell.m */; };
F03EF60219F19E7C00A0EE52 /* MediaManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF60119F19E7C00A0EE52 /* MediaManager.m */; };
F0465AFA1A251F85003639F9 /* RoomMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = F0465AF91A251F85003639F9 /* RoomMessage.m */; };
F04A8AD81A3B3DF4008AC915 /* RoomTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F04A8AD71A3B3DF4008AC915 /* RoomTitleView.m */; };
F04EE51F1A3A01D500C64930 /* APNSHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = F04EE51E1A3A01D500C64930 /* APNSHandler.m */; };
@ -62,6 +67,12 @@
/* Begin PBXFileReference section */
13057A57E74FD5504196F47F /* Pods-matrixConsole.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-matrixConsole.release.xcconfig"; path = "Pods/Target Support Files/Pods-matrixConsole/Pods-matrixConsole.release.xcconfig"; sourceTree = "<group>"; };
71D2E4EA1A49814B000DE015 /* MemberActionsCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MemberActionsCell.h; sourceTree = "<group>"; };
71D2E4EB1A49814B000DE015 /* MemberActionsCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MemberActionsCell.m; sourceTree = "<group>"; };
71DB9DBF1A495B6400504A09 /* MemberViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MemberViewController.h; sourceTree = "<group>"; };
71DB9DC01A495B6400504A09 /* MemberViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MemberViewController.m; sourceTree = "<group>"; };
71E94A751A5C4020009F52E5 /* PieChartView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PieChartView.h; sourceTree = "<group>"; };
71E94A761A5C4020009F52E5 /* PieChartView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PieChartView.m; sourceTree = "<group>"; };
8141B1E2401FFCC3C5B99234 /* libPods-matrixConsole.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-matrixConsole.a"; sourceTree = BUILT_PRODUCTS_DIR; };
B7EC7E45C718BF2BBCE0CF48 /* Pods-matrixConsole.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-matrixConsole.debug.xcconfig"; path = "Pods/Target Support Files/Pods-matrixConsole/Pods-matrixConsole.debug.xcconfig"; sourceTree = "<group>"; };
F00B5DB71A1B9BCE00EA1C8D /* CustomImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomImageView.h; sourceTree = "<group>"; };
@ -71,7 +82,13 @@
F01628BE19E29C660071C473 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = "<group>"; };
F01A0FF11A27314B009FAE2F /* RoomMessageComponent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMessageComponent.h; sourceTree = "<group>"; };
F01A0FF21A27314B009FAE2F /* RoomMessageComponent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMessageComponent.m; sourceTree = "<group>"; };
F021FBED1A5EF57300EA3AE6 /* MediaLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaLoader.h; sourceTree = "<group>"; };
F021FBEE1A5EF57300EA3AE6 /* MediaLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaLoader.m; sourceTree = "<group>"; };
F021FBF01A5F1F8E00EA3AE6 /* MediaManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaManager.h; sourceTree = "<group>"; };
F021FBF11A5F1F8E00EA3AE6 /* MediaManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaManager.m; sourceTree = "<group>"; };
F024098119E7D177006E741B /* tab_recents@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_recents@2x.png"; sourceTree = "<group>"; };
F02900B91A63C71E00356F7D /* ConsoleTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConsoleTools.h; sourceTree = "<group>"; };
F02900BA1A63C71E00356F7D /* ConsoleTools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConsoleTools.m; sourceTree = "<group>"; };
F02BCE221A1A5A2B00543B47 /* play.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = play.png; sourceTree = "<group>"; };
F02D707419F1DC9E007B47D3 /* RoomMemberTableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMemberTableCell.h; sourceTree = "<group>"; };
F02D707519F1DC9E007B47D3 /* RoomMemberTableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMemberTableCell.m; sourceTree = "<group>"; };
@ -91,8 +108,6 @@
F03EF5F519F171EB00A0EE52 /* SettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsViewController.m; sourceTree = "<group>"; };
F03EF5FD19F1762000A0EE52 /* RoomMessageTableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMessageTableCell.h; sourceTree = "<group>"; };
F03EF5FE19F1762000A0EE52 /* RoomMessageTableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMessageTableCell.m; sourceTree = "<group>"; };
F03EF60019F19E7C00A0EE52 /* MediaManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaManager.h; sourceTree = "<group>"; };
F03EF60119F19E7C00A0EE52 /* MediaManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaManager.m; sourceTree = "<group>"; };
F0465AF81A251F85003639F9 /* RoomMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMessage.h; sourceTree = "<group>"; };
F0465AF91A251F85003639F9 /* RoomMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMessage.m; sourceTree = "<group>"; };
F04A8AD61A3B3DF4008AC915 /* RoomTitleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomTitleView.h; sourceTree = "<group>"; };
@ -188,6 +203,19 @@
path = Assets;
sourceTree = "<group>";
};
F021FBEC1A5EF57300EA3AE6 /* API */ = {
isa = PBXGroup;
children = (
F02900B91A63C71E00356F7D /* ConsoleTools.h */,
F02900BA1A63C71E00356F7D /* ConsoleTools.m */,
F021FBED1A5EF57300EA3AE6 /* MediaLoader.h */,
F021FBEE1A5EF57300EA3AE6 /* MediaLoader.m */,
F021FBF01A5F1F8E00EA3AE6 /* MediaManager.h */,
F021FBF11A5F1F8E00EA3AE6 /* MediaManager.m */,
);
path = API;
sourceTree = "<group>";
};
F03EF5E919F171EB00A0EE52 /* ViewController */ = {
isa = PBXGroup;
children = (
@ -197,6 +225,8 @@
F03EF5ED19F171EB00A0EE52 /* LoginViewController.m */,
F03EF5EE19F171EB00A0EE52 /* MasterTabBarController.h */,
F03EF5EF19F171EB00A0EE52 /* MasterTabBarController.m */,
71DB9DBF1A495B6400504A09 /* MemberViewController.h */,
71DB9DC01A495B6400504A09 /* MemberViewController.m */,
F03EF5F019F171EB00A0EE52 /* RecentsViewController.h */,
F03EF5F119F171EB00A0EE52 /* RecentsViewController.m */,
F03EF5F219F171EB00A0EE52 /* RoomViewController.h */,
@ -212,6 +242,10 @@
children = (
F00B5DB71A1B9BCE00EA1C8D /* CustomImageView.h */,
F00B5DB81A1B9BCE00EA1C8D /* CustomImageView.m */,
71D2E4EA1A49814B000DE015 /* MemberActionsCell.h */,
71D2E4EB1A49814B000DE015 /* MemberActionsCell.m */,
71E94A751A5C4020009F52E5 /* PieChartView.h */,
71E94A761A5C4020009F52E5 /* PieChartView.m */,
F0E84D3E1A1F9AEC005F2E42 /* RecentsTableViewCell.h */,
F0E84D3F1A1F9AEC005F2E42 /* RecentsTableViewCell.m */,
F02D707419F1DC9E007B47D3 /* RoomMemberTableCell.h */,
@ -262,6 +296,7 @@
F07A80D419DD9DE700B621A1 /* matrixConsole */ = {
isa = PBXGroup;
children = (
F021FBEC1A5EF57300EA3AE6 /* API */,
F0465AF71A251F85003639F9 /* Model */,
F03EF5FC19F1762000A0EE52 /* View */,
F03EF5E919F171EB00A0EE52 /* ViewController */,
@ -275,8 +310,6 @@
F03C47101A02952800E445AB /* CustomAlert.m */,
F05B955D19DEED8A008761B0 /* MatrixHandler.h */,
F05B955E19DEED8A008761B0 /* MatrixHandler.m */,
F03EF60019F19E7C00A0EE52 /* MediaManager.h */,
F03EF60119F19E7C00A0EE52 /* MediaManager.m */,
F07A80E219DD9DE700B621A1 /* Main.storyboard */,
F07A80E519DD9DE700B621A1 /* Images.xcassets */,
F07A80E719DD9DE700B621A1 /* LaunchScreen.xib */,
@ -466,15 +499,19 @@
F03EF5FF19F1762000A0EE52 /* RoomMessageTableCell.m in Sources */,
F07A80D819DD9DE700B621A1 /* main.m in Sources */,
F05B955F19DEED8A008761B0 /* MatrixHandler.m in Sources */,
F021FBEF1A5EF57300EA3AE6 /* MediaLoader.m in Sources */,
F03EF5FB19F171EB00A0EE52 /* SettingsViewController.m in Sources */,
F02900BB1A63C71E00356F7D /* ConsoleTools.m in Sources */,
F01A0FF31A27314B009FAE2F /* RoomMessageComponent.m in Sources */,
F03EF5FA19F171EB00A0EE52 /* RoomViewController.m in Sources */,
F03EF5F819F171EB00A0EE52 /* MasterTabBarController.m in Sources */,
F03EF5F619F171EB00A0EE52 /* HomeViewController.m in Sources */,
71DB9DC11A495B6400504A09 /* MemberViewController.m in Sources */,
F0D942F61A31F3A300826CC1 /* RecentRoom.m in Sources */,
F03EF60219F19E7C00A0EE52 /* MediaManager.m in Sources */,
F03EF5F919F171EB00A0EE52 /* RecentsViewController.m in Sources */,
71E94A771A5C4020009F52E5 /* PieChartView.m in Sources */,
F0465AFA1A251F85003639F9 /* RoomMessage.m in Sources */,
F021FBF21A5F1F8E00EA3AE6 /* MediaManager.m in Sources */,
F04EE51F1A3A01D500C64930 /* APNSHandler.m in Sources */,
F03C47111A02952800E445AB /* CustomAlert.m in Sources */,
F0E84D401A1F9AEC005F2E42 /* RecentsTableViewCell.m in Sources */,
@ -483,6 +520,7 @@
F0D3C30C1A011EF10000D49E /* AppSettings.m in Sources */,
F03EF5F719F171EB00A0EE52 /* LoginViewController.m in Sources */,
F0D3C30F1A01330F0000D49E /* SettingsTableViewCell.m in Sources */,
71D2E4EC1A49814B000DE015 /* MemberActionsCell.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -0,0 +1,27 @@
/*
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 ConsoleTools : NSObject
// Time interval
+ (NSString*)formatSecondsInterval:(CGFloat)secondsInterval;
// Image
+ (UIImage *)resize:(UIImage *)image toFitInSize:(CGSize)size;
@end

View file

@ -0,0 +1,91 @@
/*
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 "ConsoleTools.h"
@implementation ConsoleTools
#pragma mark - Time interval
+ (NSString*)formatSecondsInterval:(CGFloat)secondsInterval {
NSMutableString* formattedString = [[NSMutableString alloc] init];
if (secondsInterval < 1) {
[formattedString appendString:@"< 1s"];
} else if (secondsInterval < 60)
{
[formattedString appendFormat:@"%ds", (int)secondsInterval];
}
else if (secondsInterval < 3600)
{
[formattedString appendFormat:@"%dm %2ds", (int)(secondsInterval/60), ((int)secondsInterval) % 60];
}
else if (secondsInterval >= 3600)
{
[formattedString appendFormat:@"%dh %dm %ds", (int)(secondsInterval / 3600),
((int)(secondsInterval) % 3600) / 60,
(int)(secondsInterval) % 60];
}
[formattedString appendString:@" left"];
return formattedString;
}
#pragma mark - Image
+ (UIImage *)resize:(UIImage *)image toFitInSize:(CGSize)size {
UIImage *resizedImage = image;
// Check whether resize is required
if (size.width && size.height) {
CGFloat width = image.size.width;
CGFloat height = image.size.height;
if (width > size.width) {
height = (height * size.width) / width;
height = floorf(height / 2) * 2;
width = size.width;
}
if (height > size.height) {
width = (width * size.height) / height;
width = floorf(width / 2) * 2;
height = size.height;
}
if (width != image.size.width || height != image.size.height) {
// Create the thumbnail
CGSize imageSize = CGSizeMake(width, height);
UIGraphicsBeginImageContext(imageSize);
// // set to the top quality
// CGContextRef context = UIGraphicsGetCurrentContext();
// CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGRect thumbnailRect = CGRectMake(0, 0, 0, 0);
thumbnailRect.origin = CGPointMake(0.0,0.0);
thumbnailRect.size.width = imageSize.width;
thumbnailRect.size.height = imageSize.height;
[image drawInRect:thumbnailRect];
resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
}
return resizedImage;
}
@end

View file

@ -0,0 +1,91 @@
/*
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>
// Provide the download progress
// object: URL
// userInfo: kMediaLoaderProgressRateKey : progress value nested in a NSNumber (range 0->1)
// : kMediaLoaderProgressStringKey : progress string XXX KB / XXX MB" (optional)
// : kMediaLoaderProgressRemaingTimeKey : remaining time string "XX s left" (optional)
// : kMediaLoaderProgressDownloadRateKey : string like XX MB/s (optional)
extern NSString *const kMediaDownloadProgressNotification;
// Provide the upload progress
// object: uploadId
// userInfo: kMediaLoaderProgressRateKey : progress value nested in a NSNumber (range 0->1)
// : kMediaLoaderProgressStringKey : progress string XXX KB / XXX MB" (optional)
// : kMediaLoaderProgressRemaingTimeKey : remaining time string "XX s left" (optional)
// : kMediaLoaderProgressDownloadRateKey : string like XX MB/s (optional)
extern NSString *const kMediaUploadProgressNotification;
// userInfo keys
extern NSString *const kMediaLoaderProgressRateKey;
extern NSString *const kMediaLoaderProgressStringKey;
extern NSString *const kMediaLoaderProgressRemaingTimeKey;
extern NSString *const kMediaLoaderProgressDownloadRateKey;
// The callback blocks
typedef void (^blockMediaLoader_onSuccess)(NSString *url); // url is a cache file path for successful download, or a remote url for upload.
typedef void (^blockMediaLoader_onError)(NSError *error);
@interface MediaLoader : NSObject <NSURLConnectionDataDelegate> {
NSString *mimeType;
blockMediaLoader_onSuccess onSuccess;
blockMediaLoader_onError onError;
// Download
NSString *mediaURL;
long long expectedSize;
NSMutableData *downloadData;
NSURLConnection *downloadConnection;
// statistic info (bitrate, remaining time...)
CFAbsoluteTime statsStartTime;
CFAbsoluteTime downloadStartTime;
CFAbsoluteTime lastProgressEventTimeStamp;
NSTimer* progressCheckTimer;
// Upload
NSString *uploadId;
CGFloat initialRange;
CGFloat range;
}
@property (strong, readonly) NSMutableDictionary* statisticsDict;
- (void)cancel;
// Download
- (void)downloadMedia:(NSString *)aMediaURL
mimeType:(NSString *)aMimeType
success:(blockMediaLoader_onSuccess)success
failure:(blockMediaLoader_onError)failure;
// Upload
// initialRange / range: an upload could be a subpart of uploads. initialRange defines the global upload progress already did done before this current upload.
// range is the range value of this upload in the global scope.
// e.g. : Upload a media can be split in two parts :
// 1 - upload the thumbnail -> initialRange = 0, range = 0.1 : assume that the thumbnail upload is 10% of the upload process
// 2 - upload the media -> initialRange = 0.1, range = 0.9 : the media upload is 90% of the global upload
- (id)initWithUploadId:(NSString *)anUploadId initialRange:(CGFloat)anInitialRange andRange:(CGFloat)aRange;
- (void)uploadData:(NSData *)data
mimeType:(NSString *)aMimeType
success:(blockMediaLoader_onSuccess)success
failure:(blockMediaLoader_onError)failure;
@end

View file

@ -0,0 +1,259 @@
/*
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 "MediaManager.h"
#import "MatrixHandler.h"
#import "ConsoleTools.h"
NSString *const kMediaDownloadProgressNotification = @"kMediaDownloadProgressNotification";
NSString *const kMediaUploadProgressNotification = @"kMediaUploadProgressNotification";
NSString *const kMediaLoaderProgressRateKey = @"kMediaLoaderProgressRateKey";
NSString *const kMediaLoaderProgressStringKey = @"kMediaLoaderProgressStringKey";
NSString *const kMediaLoaderProgressRemaingTimeKey = @"kMediaLoaderProgressRemaingTimeKey";
NSString *const kMediaLoaderProgressDownloadRateKey = @"kMediaLoaderProgressDownloadRateKey";
@implementation MediaLoader
@synthesize statisticsDict;
- (NSString*)validateContentURL:(NSString*)contentURL {
// Detect matrix content url
if ([contentURL hasPrefix:MX_PREFIX_CONTENT_URI]) {
NSString *mxMediaPrefix = [NSString stringWithFormat:@"%@%@/download/", [[MatrixHandler sharedHandler] homeServerURL], kMXMediaPathPrefix];
// Set actual url
return [contentURL stringByReplacingOccurrencesOfString:MX_PREFIX_CONTENT_URI withString:mxMediaPrefix];
}
return contentURL;
}
- (void)cancel {
// Cancel potential connection
if (downloadConnection) {
NSLog(@"Image download has been cancelled (%@)", mediaURL);
if (onError){
onError(nil);
}
// Reset blocks
onSuccess = nil;
onError = nil;
[downloadConnection cancel];
downloadConnection = nil;
downloadData = nil;
}
else {
// Reset blocks
onSuccess = nil;
onError = nil;
}
statisticsDict = nil;
}
- (void)dealloc {
[self cancel];
}
#pragma mark - Download
- (void)downloadMedia:(NSString*)aMediaURL
mimeType:(NSString *)aMimeType
success:(blockMediaLoader_onSuccess)success
failure:(blockMediaLoader_onError)failure {
// Report provided params
mediaURL = aMediaURL;
mimeType = aMimeType;
onSuccess = success;
onError = failure;
downloadStartTime = statsStartTime = CFAbsoluteTimeGetCurrent();
lastProgressEventTimeStamp = -1;
// Start downloading
NSURL *url = [NSURL URLWithString:[self validateContentURL:aMediaURL]];
downloadData = [[NSMutableData alloc] init];
downloadConnection = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
expectedSize = response.expectedContentLength;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"ERROR: media download failed: %@, %@", error, mediaURL);
// send the latest known upload info
[self progressCheckTimeout:nil];
statisticsDict = nil;
if (onError) {
onError (error);
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Append data
[downloadData appendData:data];
if (expectedSize > 0) {
//
float rate = ((float)downloadData.length) / ((float)expectedSize);
// should never happen
if (rate > 1) {
rate = 1.0;
}
CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent();
CGFloat deltaTime = currentTime - statsStartTime;
// in KB
float dataRate;
if (deltaTime > 0)
{
dataRate = ((CGFloat)data.length) / deltaTime / 1024.0;
}
else // avoid zero div error
{
dataRate = ((CGFloat)data.length) / (0.001) / 1024.0;
}
CGFloat meanRate = downloadData.length / (currentTime - downloadStartTime)/ 1024.0;
CGFloat dataRemainingTime = 0;
if (0 != meanRate)
{
dataRemainingTime = ((expectedSize - downloadData.length) / 1024.0) / meanRate;
}
statsStartTime = currentTime;
// build the user info dictionary
NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
[dict setValue:[NSNumber numberWithFloat:rate] forKey:kMediaLoaderProgressRateKey];
NSString* progressString = [NSString stringWithFormat:@"%@ / %@", [NSByteCountFormatter stringFromByteCount:downloadData.length countStyle:NSByteCountFormatterCountStyleFile], [NSByteCountFormatter stringFromByteCount:expectedSize countStyle:NSByteCountFormatterCountStyleFile]];
[dict setValue:progressString forKey:kMediaLoaderProgressStringKey];
[dict setValue:[ConsoleTools formatSecondsInterval:dataRemainingTime] forKey:kMediaLoaderProgressRemaingTimeKey];
NSString* downloadRateStr = [NSString stringWithFormat:@"%@/s", [NSByteCountFormatter stringFromByteCount:meanRate * 1024 countStyle:NSByteCountFormatterCountStyleFile]];
[dict setValue:downloadRateStr forKey:kMediaLoaderProgressDownloadRateKey];
statisticsDict = dict;
// after 0.1s, resend the progress info
// the upload can be stuck
[progressCheckTimer invalidate];
progressCheckTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(progressCheckTimeout:) userInfo:self repeats:NO];
// trigger the event only each 0.1s to avoid send to many events
if ((lastProgressEventTimeStamp == -1) || ((currentTime - lastProgressEventTimeStamp) > 0.1)) {
lastProgressEventTimeStamp = currentTime;
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:statisticsDict];
}
}
}
- (IBAction)progressCheckTimeout:(id)sender {
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadProgressNotification object:mediaURL userInfo:statisticsDict];
[progressCheckTimer invalidate];
progressCheckTimer = nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// send the latest known upload info
[self progressCheckTimeout:nil];
statisticsDict = nil;
if (downloadData.length) {
// Cache the downloaded data
NSString *cacheFilePath = [MediaManager cacheMediaData:downloadData forURL:mediaURL andType:mimeType];
// Call registered block
if (onSuccess) {
onSuccess(cacheFilePath);
}
} else {
NSLog(@"ERROR: media download failed: %@", mediaURL);
if (onError){
onError(nil);
}
}
downloadData = nil;
downloadConnection = nil;
}
#pragma mark - Upload
- (id)initWithUploadId:(NSString *)anUploadId initialRange:(CGFloat)anInitialRange andRange:(CGFloat)aRange {
if (self = [super init]) {
uploadId = anUploadId;
initialRange = anInitialRange;
range = aRange;
}
return self;
}
- (void)uploadData:(NSData *)data mimeType:(NSString *)aMimeType success:(blockMediaLoader_onSuccess)success failure:(blockMediaLoader_onError)failure {
mimeType = aMimeType;
statsStartTime = CFAbsoluteTimeGetCurrent();
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxRestClient uploadContent:data mimeType:mimeType timeout:30 success:success failure:failure uploadProgress:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
[self updateUploadProgressWithBytesWritten:bytesWritten totalBytesWritten:totalBytesWritten andTotalBytesExpectedToWrite:totalBytesExpectedToWrite];
}];
}
- (void)updateUploadProgressWithBytesWritten:(NSUInteger)bytesWritten totalBytesWritten:(long long)totalBytesWritten andTotalBytesExpectedToWrite:(long long)totalBytesExpectedToWrite {
CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent();
if (!statisticsDict) {
statisticsDict = [[NSMutableDictionary alloc] init];
}
CGFloat progressRate = initialRange + (((float)totalBytesWritten) / ((float)totalBytesExpectedToWrite) * range);
[statisticsDict setValue:[NSNumber numberWithFloat:progressRate] forKey:kMediaLoaderProgressRateKey];
CGFloat dataRate = 0;
if (currentTime != statsStartTime)
{
dataRate = bytesWritten / 1024.0 / (currentTime - statsStartTime);
}
else
{
dataRate = bytesWritten / 1024.0 / 0.001;
}
statsStartTime = currentTime;
CGFloat dataRemainingTime = 0;
if (0 != dataRate)
{
dataRemainingTime = (totalBytesExpectedToWrite - totalBytesWritten)/ 1024.0 / dataRate;
}
NSString* progressString = [NSString stringWithFormat:@"%@ / %@", [NSByteCountFormatter stringFromByteCount:totalBytesWritten countStyle:NSByteCountFormatterCountStyleFile], [NSByteCountFormatter stringFromByteCount:totalBytesExpectedToWrite countStyle:NSByteCountFormatterCountStyleFile]];
[statisticsDict setValue:progressString forKey:kMediaLoaderProgressStringKey];
[statisticsDict setValue:[ConsoleTools formatSecondsInterval:dataRemainingTime] forKey:kMediaLoaderProgressRemaingTimeKey];
NSString* downloadRateStr = [NSString stringWithFormat:@"%@/s", [NSByteCountFormatter stringFromByteCount:dataRate * 1024 countStyle:NSByteCountFormatterCountStyleFile]];
[statisticsDict setValue:downloadRateStr forKey:kMediaLoaderProgressDownloadRateKey];
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaUploadProgressNotification object:uploadId userInfo:statisticsDict];
}
@end

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 <AVFoundation/AVFoundation.h>
#import "MediaLoader.h"
extern NSString *const kMediaManagerPrefixForDummyURL;
// notify when a media download is finished (object: URL)
extern NSString *const kMediaDownloadDidFinishNotification;
extern NSString *const kMediaDownloadDidFailNotification;
@interface MediaManager : NSObject
// Download data from the provided URL. Return a mediaLoader reference in order to let the user cancel this action.
+ (MediaLoader*)downloadMediaFromURL:(NSString *)mediaURL withType:(NSString *)mimeType;
// Check whether a download is already running for this media url. Return loader if any
+ (MediaLoader*)existingDownloaderForURL:(NSString*)url;
// Prepares and returns a media loader to upload data to matrix content repository.
// initialRange / range: an upload could be a subpart of uploads. initialRange defines the global upload progress already did done before this current upload.
// range is the range value of this upload in the global scope.
// e.g. : Upload a media can be split in two parts :
// 1 - upload the thumbnail -> initialRange = 0, range = 0.1 : assume that the thumbnail upload is 10% of the upload process
// 2 - upload the media -> initialRange = 0.1, range = 0.9 : the media upload is 90% of the global upload
+ (MediaLoader*)prepareUploaderWithId:(NSString *)uploadId initialRange:(CGFloat)initialRange andRange:(CGFloat)range;
// Check whether an upload is already running with this id. Return loader if any
+ (MediaLoader*)existingUploaderWithId:(NSString*)uploadId;
+ (void)removeUploaderWithId:(NSString*)uploadId;
// Load a picture from the local cache (Do not start any remote requests)
+ (UIImage*)loadCachePictureForURL:(NSString*)pictureURL;
// Store in cache the provided data for the media URL, return the path of the resulting file
+ (NSString*)cacheMediaData:(NSData *)mediaData forURL:(NSString *)mediaURL andType:(NSString *)mimeType;
// Return the cache path deduced from media URL and type
+ (NSString*)cachePathForMediaURL:(NSString*)mediaURL andType:(NSString *)mimeType;
+ (NSUInteger)cacheSize;
+ (void)clearCache;
@end

View file

@ -0,0 +1,224 @@
/*
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 "MediaManager.h"
NSString *const kMediaManagerPrefixForDummyURL = @"dummyUrl-";
NSString *const kMediaDownloadDidFinishNotification = @"kMediaDownloadDidFinishNotification";
NSString *const kMediaDownloadDidFailNotification = @"kMediaDownloadDidFailNotification";
static NSString* mediaCachePath = nil;
static NSString *mediaDir = @"mediacache";
static MediaManager *sharedMediaManager = nil;
@implementation MediaManager
// Table of downloads in progress
static NSMutableDictionary* downloadTableByURL = nil;
// Table of uploads in progress
static NSMutableDictionary* uploadTableById = nil;
#pragma mark - Media Download
+ (MediaLoader*)downloadMediaFromURL:(NSString*)mediaURL withType:(NSString *)mimeType {
if (mediaURL && [mediaURL hasPrefix:kMediaManagerPrefixForDummyURL] == NO) {
// Create a media loader to download data
MediaLoader *mediaLoader = [[MediaLoader alloc] init];
// Report this loader
if (!downloadTableByURL) {
downloadTableByURL = [[NSMutableDictionary alloc] init];
}
[downloadTableByURL setValue:mediaLoader forKey:mediaURL];
// Launch download
[mediaLoader downloadMedia:mediaURL mimeType:mimeType success:^(NSString *cacheFilePath) {
[downloadTableByURL removeObjectForKey:mediaURL];
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFinishNotification object:mediaURL userInfo:nil];
} failure:^(NSError *error) {
[downloadTableByURL removeObjectForKey:mediaURL];
NSLog(@"Failed to download image (%@): %@", mediaURL, error);
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:mediaURL userInfo:nil];
}];
return mediaLoader;
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMediaDownloadDidFailNotification object:mediaURL userInfo:nil];
return nil;
}
// try to find out a media loader from a media URL
+ (id)existingDownloaderForURL:(NSString*)url {
if (downloadTableByURL && url) {
return [downloadTableByURL valueForKey:url];
}
return nil;
}
#pragma mark - Media Uploader
+ (MediaLoader*)prepareUploaderWithId:(NSString *)uploadId initialRange:(CGFloat)initialRange andRange:(CGFloat)range {
if (uploadId) {
// Create a media loader to upload data
MediaLoader *mediaLoader = [[MediaLoader alloc] initWithUploadId:uploadId initialRange:initialRange andRange:range];
// Report this loader
if (!uploadTableById) {
uploadTableById = [[NSMutableDictionary alloc] init];
}
[uploadTableById setValue:mediaLoader forKey:uploadId];
return mediaLoader;
}
return nil;
}
+ (MediaLoader*)existingUploaderWithId:(NSString*)uploadId {
if (uploadTableById && uploadId) {
return [uploadTableById valueForKey:uploadId];
}
return nil;
}
+ (void)removeUploaderWithId:(NSString*)uploadId {
if (uploadTableById && uploadId) {
return [uploadTableById removeObjectForKey:uploadId];
}
}
#pragma mark - Cache Handling
+ (UIImage*)loadCachePictureForURL:(NSString*)pictureURL {
UIImage* res = nil;
NSString* filename = [MediaManager cachePathForMediaURL:pictureURL andType:@"image/jpeg"];
if ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
NSData* imageContent = [NSData dataWithContentsOfFile:filename options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
if (imageContent) {
res = [[UIImage alloc] initWithData:imageContent];
}
}
return res;
}
+ (NSString*)cacheMediaData:(NSData*)mediaData forURL:(NSString *)mediaURL andType:(NSString *)mimeType {
NSString* filename = [MediaManager cachePathForMediaURL:mediaURL andType:mimeType];
if ([mediaData writeToFile:filename atomically:YES]) {
return filename;
} else {
return nil;
}
}
+ (NSString*)cachePathForMediaURL:(NSString*)mediaURL andType:(NSString *)mimeType {
NSString *fileName;
if ([mimeType isEqualToString:@"image/jpeg"]) {
fileName = [NSString stringWithFormat:@"ima%lu.jpg", (unsigned long)mediaURL.hash];
} else if ([mimeType isEqualToString:@"video/mp4"]) {
fileName = [NSString stringWithFormat:@"video%lu.mp4", (unsigned long)mediaURL.hash];
} else if ([mimeType isEqualToString:@"video/quicktime"]) {
fileName = [NSString stringWithFormat:@"video%lu.mov", (unsigned long)mediaURL.hash];
} else {
NSString *extension = @"";
NSArray *components = [mediaURL componentsSeparatedByString:@"."];
if (components && components.count > 1) {
extension = [components lastObject];
}
fileName = [NSString stringWithFormat:@"%lu.%@", (unsigned long)mediaURL.hash, extension];
}
return [[MediaManager getCachePath] stringByAppendingPathComponent:fileName];
}
+ (void)clearCache {
NSError *error = nil;
if (!mediaCachePath) {
// compute the path
mediaCachePath = [MediaManager getCachePath];
}
if (mediaCachePath) {
if (![[NSFileManager defaultManager] removeItemAtPath:mediaCachePath error:&error]) {
NSLog(@"Fails to delete media cache dir : %@", error);
} else {
NSLog(@"Media cache : deleted !");
}
} else {
NSLog(@"Media cache does not exist");
}
mediaCachePath = nil;
}
// recursive method to compute the folder content size
+ (long long)folderSize:(NSString *)folderPath
{
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:folderPath error:nil];
NSEnumerator *contentsEnumurator = [contents objectEnumerator];
NSString *file;
unsigned long long int folderSize = 0;
while (file = [contentsEnumurator nextObject])
{
NSString* itemPath = [folderPath stringByAppendingPathComponent:file];
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:nil];
// is directory
if ([[fileAttributes objectForKey:NSFileType] isEqual:NSFileTypeDirectory])
{
folderSize += [MediaManager folderSize:itemPath];
}
else
{
folderSize += [[fileAttributes objectForKey:NSFileSize] intValue];
}
}
return folderSize;
}
+ (NSUInteger)cacheSize {
if (!mediaCachePath) {
// compute the path
mediaCachePath = [MediaManager getCachePath];
}
return (NSUInteger)[MediaManager folderSize:mediaCachePath];
}
+ (NSString*)getCachePath {
NSString *cachePath = nil;
if (!mediaCachePath) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheRoot = [paths objectAtIndex:0];
mediaCachePath = [cacheRoot stringByAppendingPathComponent:mediaDir];
if (![[NSFileManager defaultManager] fileExistsAtPath:mediaCachePath]) {
[[NSFileManager defaultManager] createDirectoryAtPath:mediaCachePath withIntermediateDirectories:NO attributes:nil error:nil];
}
}
cachePath = mediaCachePath;
return cachePath;
}
@end

View file

@ -91,7 +91,7 @@ static APNSHandler *sharedHandler = nil;
- (void)setIsActive:(BOOL)isActive {
// Refuse to try & turn push on if we're not logged in, it's nonsensical.
if (![[MatrixHandler sharedHandler] isLogged]) {
if ([MatrixHandler sharedHandler].status == MatrixHandlerStatusLoggedOut) {
NSLog(@"Not logged in: not setting push token because we're not logged in");
return;
}

View file

@ -20,6 +20,7 @@
#import "RoomViewController.h"
#import "MatrixHandler.h"
#import "MediaManager.h"
#import "SettingsViewController.h"
@interface AppDelegate () <UISplitViewControllerDelegate>
@ -52,6 +53,14 @@
// IOS >= 8
if ([splitViewController respondsToSelector:@selector(displayModeButtonItem)]) {
navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem;
// on IOS 8 iPad devices, force to display the primary and the secondary viewcontroller
// to avoid empty room View Controller in portrait orientation
// else, the user cannot select a room
// shouldHideViewController delegate method is also implemented
if ([splitViewController respondsToSelector:@selector(preferredDisplayMode)] && [(NSString*)[UIDevice currentDevice].model hasPrefix:@"iPad"]) {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
}
}
splitViewController.delegate = self;
@ -67,8 +76,10 @@
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
[[NSUserDefaults standardUserDefaults] synchronize];
if ([[MatrixHandler sharedHandler] isLogged]) {
if ([MatrixHandler sharedHandler].status != MatrixHandlerStatusLoggedOut) {
[self registerUserNotificationSettings];
// When user is already logged, we launch the app on Recents
[self.masterTabBarController setSelectedIndex:TABBAR_RECENTS_INDEX];
}
}
return YES;
@ -81,14 +92,14 @@
[self.errorNotification dismiss:NO];
self.errorNotification = nil;
}
// Suspend Matrix handler
[[MatrixHandler sharedHandler] pauseInBackgroundTask];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
// Suspend Matrix handler
[[MatrixHandler sharedHandler] pauseInBackgroundTask];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
@ -192,7 +203,7 @@
return self.errorNotification;
}
#pragma mark - Split view
#pragma mark - SplitViewController delegate
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[RoomViewController class]] && ([(RoomViewController *)[(UINavigationController *)secondaryViewController topViewController] roomId] == nil)) {
@ -203,4 +214,34 @@
}
}
- (BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation {
// oniPad devices, force to display the primary and the secondary viewcontroller
// to avoid empty room View Controller in portrait orientation
// else, the user cannot select a room
return NO;
}
#pragma mark - UITabBarControllerDelegate delegate
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
BOOL res = YES;
UIViewController* currentViewController = [tabBarController.viewControllers objectAtIndex:tabBarController.selectedIndex];
if ([currentViewController isKindOfClass:[UINavigationController class]]) {
UIViewController *topViewController = ((UINavigationController*)currentViewController).topViewController;
// ask to the user to save unsaved profile updates
// before switching to another tab
if ([topViewController isKindOfClass:[SettingsViewController class]]) {
__block NSUInteger nextSelectedViewController = [tabBarController.viewControllers indexOfObject:viewController];
res = ![((SettingsViewController *)topViewController) checkPendingSave:^() {
tabBarController.selectedIndex = nextSelectedViewController;
}];
}
}
return res;
}
@end

View file

@ -18,10 +18,11 @@
@interface AppSettings : NSObject
@property (nonatomic) BOOL enableInAppNotifications;
@property (nonatomic) NSArray* specificWordsToAlertOn;
@property (nonatomic) BOOL displayAllEvents;
@property (nonatomic) BOOL hideUnsupportedMessages;
@property (nonatomic) BOOL sortMembersUsingLastSeenTime;
@property (nonatomic) BOOL displayLeftUsers;
@property (nonatomic) BOOL displayLeftUsers;
+ (AppSettings *)sharedSettings;

View file

@ -44,6 +44,7 @@ static AppSettings *sharedSettings = nil;
- (void)reset {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"enableInAppNotifications"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"specificWordsToAlertOn"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"displayAllEvents"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"hideUnsupportedMessages"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"sortMembersUsingLastSeenTime"];
@ -62,6 +63,26 @@ static AppSettings *sharedSettings = nil;
[[NSUserDefaults standardUserDefaults] setBool:notifications forKey:@"enableInAppNotifications"];
}
- (NSArray*)specificWordsToAlertOn {
NSArray* res = [[NSUserDefaults standardUserDefaults] objectForKey:@"specificWordsToAlertOn"];
// avoid returning nil
if (!res) {
res = [[NSArray alloc] init];
}
return res;
}
- (void)setSpecificWordsToAlertOn:(NSArray*)words {
if (!words.count) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"specificWordsToAlertOn"];
} else {
[[NSUserDefaults standardUserDefaults] setObject:words forKey:@"specificWordsToAlertOn"];
}
}
- (BOOL)displayAllEvents {
return [[NSUserDefaults standardUserDefaults] boolForKey:@"displayAllEvents"];
}
@ -69,7 +90,7 @@ static AppSettings *sharedSettings = nil;
- (void)setDisplayAllEvents:(BOOL)displayAllEvents {
[[NSUserDefaults standardUserDefaults] setBool:displayAllEvents forKey:@"displayAllEvents"];
// Flush and restore Matrix data
[[MatrixHandler sharedHandler] forceInitialSync];
[[MatrixHandler sharedHandler] forceInitialSync:NO];
}
- (BOOL)hideUnsupportedMessages {

View file

@ -5,6 +5,7 @@
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
</dependencies>
<scenes>
<!--Recents-->
@ -95,15 +96,46 @@
<rect key="frame" x="531" y="10" width="61" height="40"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yUK-od-5YZ" userLabel="ProgressView">
<rect key="frame" x="487" y="-1" width="100" height="70"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rate" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" minimumFontSize="4" preferredMaxLayoutWidth="100" translatesAutoresizingMaskIntoConstraints="NO" id="HFo-GV-TO9" userLabel="Progress stats">
<rect key="frame" x="0.0" y="60" width="100" height="10"/>
<fontDescription key="fontDescription" type="system" pointSize="8"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pg4-aQ-7qW" customClass="PieChartView">
<rect key="frame" x="30" y="0.0" width="40" height="40"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="DnA-mx-Lyj"/>
<constraint firstAttribute="height" constant="40" id="Z4v-m3-EOx"/>
</constraints>
</view>
</subviews>
<gestureRecognizers/>
<constraints>
<constraint firstItem="HFo-GV-TO9" firstAttribute="leading" secondItem="yUK-od-5YZ" secondAttribute="leading" id="2Xg-Bu-iAg"/>
<constraint firstAttribute="centerX" secondItem="pg4-aQ-7qW" secondAttribute="centerX" id="FQt-WU-G5b"/>
<constraint firstAttribute="width" constant="100" id="Ftj-Ew-rWQ"/>
<constraint firstAttribute="centerX" secondItem="HFo-GV-TO9" secondAttribute="centerX" id="KhM-B0-Lqp"/>
<constraint firstAttribute="bottom" secondItem="HFo-GV-TO9" secondAttribute="bottom" id="dzX-eg-0Dc"/>
<constraint firstAttribute="height" constant="70" id="epL-98-VdI"/>
<constraint firstItem="pg4-aQ-7qW" firstAttribute="top" secondItem="yUK-od-5YZ" secondAttribute="top" id="mEw-ej-VSR"/>
<constraint firstAttribute="trailing" secondItem="HFo-GV-TO9" secondAttribute="trailing" id="rF7-vW-wgF"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="mvK-ez-meg" firstAttribute="centerY" secondItem="yUK-od-5YZ" secondAttribute="centerY" id="3bj-yG-8A6"/>
<constraint firstAttribute="bottom" secondItem="J5R-Mh-3hV" secondAttribute="bottom" id="662-Ze-6ia"/>
<constraint firstItem="Ttt-0P-dQW" firstAttribute="leading" secondItem="egJ-aY-QVW" secondAttribute="trailing" id="6L3-Pz-zbG"/>
<constraint firstItem="ds0-yH-8Uu" firstAttribute="top" secondItem="iJp-sA-hG6" secondAttribute="top" constant="10" id="998-YZ-TJ4"/>
<constraint firstItem="mvK-ez-meg" firstAttribute="top" secondItem="iJp-sA-hG6" secondAttribute="top" constant="18" id="AeJ-P9-ueq"/>
<constraint firstItem="ds0-yH-8Uu" firstAttribute="leading" secondItem="egJ-aY-QVW" secondAttribute="trailing" id="G6w-Dp-rpr"/>
<constraint firstAttribute="bottom" secondItem="Ttt-0P-dQW" secondAttribute="bottom" id="GAU-J5-ciT"/>
<constraint firstAttribute="trailingMargin" secondItem="yUK-od-5YZ" secondAttribute="trailing" constant="5" id="HKC-g7-1nd"/>
<constraint firstItem="egJ-aY-QVW" firstAttribute="top" secondItem="iJp-sA-hG6" secondAttribute="top" constant="3" id="N8f-0n-ObR"/>
<constraint firstItem="Ttt-0P-dQW" firstAttribute="top" secondItem="iJp-sA-hG6" secondAttribute="top" id="Ptt-qa-Cg4"/>
<constraint firstItem="mvK-ez-meg" firstAttribute="centerY" secondItem="vF4-rq-4Rn" secondAttribute="centerY" id="ROj-jF-hIQ"/>
@ -133,6 +165,9 @@
<outlet property="msgTextViewTopConstraint" destination="rJt-w3-D8g" id="6Um-o1-J08"/>
<outlet property="pictureView" destination="uhu-R0-9NH" id="59O-If-m7H"/>
<outlet property="playIconView" destination="vF4-rq-4Rn" id="G3R-52-GmA"/>
<outlet property="progressChartView" destination="pg4-aQ-7qW" id="pdM-fl-r2e"/>
<outlet property="progressView" destination="yUK-od-5YZ" id="Qba-ld-tjt"/>
<outlet property="statsLabel" destination="HFo-GV-TO9" id="pu0-DB-zgG"/>
<outlet property="userNameLabel" destination="egJ-aY-QVW" id="IWg-7t-5Vp"/>
</connections>
</tableViewCell>
@ -165,6 +200,35 @@
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="Pq8-lB-cZM">
<rect key="frame" x="443" y="24" width="20" height="20"/>
</activityIndicatorView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="own-aM-Zlf" userLabel="ProgressView">
<rect key="frame" x="18" y="-1" width="100" height="70"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="text" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" minimumFontSize="4" preferredMaxLayoutWidth="100" translatesAutoresizingMaskIntoConstraints="NO" id="L87-yV-XGk" userLabel="Progress stats">
<rect key="frame" x="0.0" y="60" width="100" height="10"/>
<fontDescription key="fontDescription" type="system" pointSize="8"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lTG-14-OWN" customClass="PieChartView">
<rect key="frame" x="30" y="0.0" width="40" height="40"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="egQ-Kd-VbD"/>
<constraint firstAttribute="height" constant="40" id="k9p-HS-FTd"/>
</constraints>
</view>
</subviews>
<gestureRecognizers/>
<constraints>
<constraint firstItem="L87-yV-XGk" firstAttribute="leading" secondItem="own-aM-Zlf" secondAttribute="leading" id="BFY-SA-2gW"/>
<constraint firstAttribute="height" constant="70" id="Hg0-st-iq2"/>
<constraint firstAttribute="bottom" secondItem="L87-yV-XGk" secondAttribute="bottom" id="XHf-06-vkY"/>
<constraint firstAttribute="trailing" secondItem="L87-yV-XGk" secondAttribute="trailing" id="bpR-p0-f7M"/>
<constraint firstAttribute="width" constant="100" id="c7o-qp-t8x"/>
<constraint firstItem="lTG-14-OWN" firstAttribute="top" secondItem="own-aM-Zlf" secondAttribute="top" id="nNh-6v-viL"/>
<constraint firstAttribute="centerX" secondItem="L87-yV-XGk" secondAttribute="centerX" id="vMM-Hu-UkI"/>
<constraint firstAttribute="centerX" secondItem="lTG-14-OWN" secondAttribute="centerX" id="weT-lI-eKE"/>
</constraints>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="default-profile.png" translatesAutoresizingMaskIntoConstraints="NO" id="mks-jh-AiZ" customClass="CustomImageView">
<rect key="frame" x="552" y="5" width="40" height="40"/>
<constraints>
@ -193,7 +257,9 @@
<constraint firstAttribute="bottom" secondItem="Glo-Wx-mP6" secondAttribute="bottom" id="23i-Iz-P2P"/>
<constraint firstItem="7qn-gi-w7s" firstAttribute="leading" secondItem="5tf-BC-9Ed" secondAttribute="leading" constant="69" id="Fys-kP-JGR"/>
<constraint firstItem="Glo-Wx-mP6" firstAttribute="leading" secondItem="5tf-BC-9Ed" secondAttribute="leading" constant="8" id="HbL-hO-OE7"/>
<constraint firstItem="own-aM-Zlf" firstAttribute="centerY" secondItem="QZT-V8-yqJ" secondAttribute="centerY" id="Iyj-qo-ozl"/>
<constraint firstItem="Glo-Wx-mP6" firstAttribute="top" secondItem="5tf-BC-9Ed" secondAttribute="top" constant="10" id="KAT-n3-5vl"/>
<constraint firstItem="own-aM-Zlf" firstAttribute="leading" secondItem="5tf-BC-9Ed" secondAttribute="leadingMargin" constant="10" id="KFA-n1-Wj6"/>
<constraint firstAttribute="bottom" secondItem="7qn-gi-w7s" secondAttribute="bottom" id="KPt-Vo-ntg"/>
<constraint firstItem="fNQ-DX-U8F" firstAttribute="leading" secondItem="5tf-BC-9Ed" secondAttribute="leading" id="MqK-3Z-lp5"/>
<constraint firstAttribute="bottom" secondItem="fNQ-DX-U8F" secondAttribute="bottom" id="NUK-Kq-ITl"/>
@ -223,6 +289,9 @@
<outlet property="msgTextViewTopConstraint" destination="owD-KZ-snG" id="oqc-0f-05O"/>
<outlet property="pictureView" destination="mks-jh-AiZ" id="qL1-Kd-oRC"/>
<outlet property="playIconView" destination="0Bl-Sv-Q2H" id="VNa-J3-NuO"/>
<outlet property="progressChartView" destination="lTG-14-OWN" id="KQO-cZ-qvK"/>
<outlet property="progressView" destination="own-aM-Zlf" id="3vq-Cd-3Xu"/>
<outlet property="statsLabel" destination="L87-yV-XGk" id="7hG-Eb-zQr"/>
</connections>
</tableViewCell>
</prototypes>
@ -345,6 +414,7 @@
<outlet property="pictureView" destination="RW8-nh-DTj" id="1Lk-bd-tKv"/>
<outlet property="powerContainer" destination="wDo-tA-Ar7" id="sub-O0-L9d"/>
<outlet property="userLabel" destination="uVK-4R-arl" id="OhP-VD-vj0"/>
<segue destination="qlN-Mb-ZH7" kind="show" identifier="showMemberSheet" id="QMI-Ay-tFz"/>
</connections>
</tableViewCell>
</prototypes>
@ -431,6 +501,7 @@
<outlet property="activityIndicator" destination="dvT-c5-Ymf" id="F6h-Al-Vw6"/>
<outlet property="controlView" destination="6fM-aJ-d0M" id="13g-Wl-z5n"/>
<outlet property="controlViewBottomConstraint" destination="C5t-bm-3s8" id="Ks1-Z5-mzO"/>
<outlet property="membersListButtonItem" destination="3d6-ln-ICU" id="KtB-5h-eJl"/>
<outlet property="membersTableView" destination="pLY-I9-ghF" id="Ioc-IJ-WYX"/>
<outlet property="membersView" destination="OWi-J8-sFZ" id="3n2-n5-r6B"/>
<outlet property="messageTextField" destination="k2m-aY-U73" id="fSA-Eg-duj"/>
@ -445,6 +516,110 @@
</objects>
<point key="canvasLocation" x="1595" y="75"/>
</scene>
<!--DetailMember-->
<scene sceneID="q1J-Wz-aLa">
<objects>
<tableViewController id="qlN-Mb-ZH7" userLabel="DetailMember" customClass="MemberViewController" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="euy-fV-mSK">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="Dog-RG-0F9" userLabel="TableHeaderView">
<rect key="frame" x="0.0" y="64" width="600" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleAspectFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PLk-7G-Li9" userLabel="PicturePickerButton">
<rect key="frame" x="10" y="12" width="75" height="75"/>
<constraints>
<constraint firstAttribute="width" secondItem="PLk-7G-Li9" secondAttribute="height" multiplier="1:1" id="BDm-oa-tFY"/>
<constraint firstAttribute="width" constant="75" id="PEb-3J-Qb3"/>
<constraint firstAttribute="height" constant="75" id="u7H-gw-Ry4"/>
</constraints>
<state key="normal" image="default-profile.png">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="e5s-dh-H4J">
<rect key="frame" x="95" y="-26" width="495" height="151"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="e5s-dh-H4J" firstAttribute="leading" secondItem="Dog-RG-0F9" secondAttribute="leading" constant="95" id="0qf-mY-DBa"/>
<constraint firstAttribute="centerY" secondItem="e5s-dh-H4J" secondAttribute="centerY" id="Bb2-vL-ktJ"/>
<constraint firstAttribute="trailing" secondItem="e5s-dh-H4J" secondAttribute="trailing" constant="10" id="aQ9-eM-Pzu"/>
<constraint firstAttribute="centerY" secondItem="PLk-7G-Li9" secondAttribute="centerY" id="kjF-HV-ZXu"/>
<constraint firstItem="PLk-7G-Li9" firstAttribute="leading" secondItem="Dog-RG-0F9" secondAttribute="leading" constant="10" id="vp2-QW-00a"/>
</constraints>
</view>
<prototypes>
<tableViewCell contentMode="scaleToFill" restorationIdentifier="MemberActionsCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="MemberActionsCell" id="zRe-DS-U67" customClass="MemberActionsCell">
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="zRe-DS-U67" id="C5i-Xc-2Zi">
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1wm-Iq-0AV">
<rect key="frame" x="30" y="7" width="240" height="29"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<state key="normal" title="LeftButton">
<color key="titleColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<action selector="onButtonToggle:" destination="qlN-Mb-ZH7" eventType="touchUpInside" id="Wyh-9j-XfT"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fW1-mK-8cr" userLabel="rightButton">
<rect key="frame" x="330" y="7" width="247" height="29"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<state key="normal" title="rightButton">
<color key="titleColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="onButtonToggle:" destination="qlN-Mb-ZH7" eventType="touchUpInside" id="qJQ-h5-V6K"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="centerY" secondItem="1wm-Iq-0AV" secondAttribute="centerY" id="1pP-t1-J2I"/>
<constraint firstAttribute="trailing" secondItem="fW1-mK-8cr" secondAttribute="trailing" constant="30" id="2Bz-HC-j4A"/>
<constraint firstItem="fW1-mK-8cr" firstAttribute="leading" secondItem="1wm-Iq-0AV" secondAttribute="trailing" constant="60" id="Dxq-lG-e5k"/>
<constraint firstAttribute="centerX" secondItem="fW1-mK-8cr" secondAttribute="centerX" multiplier="0.66" id="KzO-HL-tLc"/>
<constraint firstItem="1wm-Iq-0AV" firstAttribute="leading" secondItem="C5i-Xc-2Zi" secondAttribute="leading" constant="30" id="L8Z-iE-e3o"/>
<constraint firstAttribute="centerY" secondItem="fW1-mK-8cr" secondAttribute="centerY" id="WUV-OQ-JTN"/>
<constraint firstAttribute="centerX" secondItem="1wm-Iq-0AV" secondAttribute="centerX" multiplier="2" id="xQc-c2-Kz6"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="2Bz-HC-j4A"/>
</mask>
</variation>
</tableViewCellContentView>
<connections>
<outlet property="leftButton" destination="1wm-Iq-0AV" id="aDO-vS-rnE"/>
<outlet property="rightButton" destination="fW1-mK-8cr" id="2xz-UI-18C"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="qlN-Mb-ZH7" id="ipl-VO-ZdA"/>
<outlet property="delegate" destination="qlN-Mb-ZH7" id="ceB-pW-r7c"/>
</connections>
</tableView>
<connections>
<outlet property="memberThumbnailButton" destination="PLk-7G-Li9" id="Rol-K2-IAS"/>
<outlet property="roomMemberMID" destination="e5s-dh-H4J" id="Upn-om-4du"/>
<outlet property="tableView" destination="euy-fV-mSK" id="MQl-lX-QSp"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="mrY-z4-HGF" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2297" y="74"/>
</scene>
<!--Home-->
<scene sceneID="3rt-8o-eGh">
<objects>
@ -500,7 +675,7 @@
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="(e.g. #foo:homeserver)" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="Rfz-WH-4KQ">
<rect key="frame" x="109" y="86" width="483" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="ldZ-75-BUU" id="cVW-bM-tAs"/>
</connections>
@ -839,26 +1014,49 @@
</connections>
</button>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Your display name" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="8M9-ZM-efS">
<rect key="frame" x="95" y="35" width="495" height="30"/>
<rect key="frame" x="95" y="13" width="495" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<action selector="textFieldDidChange:" destination="1TJ-Md-cjN" eventType="editingChanged" id="asg-M3-0vO"/>
<outlet property="delegate" destination="1TJ-Md-cjN" id="hV1-mY-vBI"/>
</connections>
</textField>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="1H7-DG-oTV">
<rect key="frame" x="290" y="40" width="20" height="20"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="T3d-LT-ivg">
<rect key="frame" x="95" y="62" width="495" height="30"/>
<state key="normal" title="Save changes">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="1TJ-Md-cjN" eventType="touchUpInside" id="QdM-AP-HIU"/>
</connections>
</button>
<view alpha="0.49999999999999961" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5as-wy-D5h" userLabel="Spinner background view">
<rect key="frame" x="0.0" y="0.0" width="600" height="100"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="1H7-DG-oTV">
<rect key="frame" x="282" y="32" width="37" height="37"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="centerY" secondItem="1H7-DG-oTV" secondAttribute="centerY" id="3oK-Ie-SK8"/>
<constraint firstAttribute="bottom" secondItem="5as-wy-D5h" secondAttribute="bottom" id="5ud-nw-rsd"/>
<constraint firstAttribute="centerY" secondItem="5Dl-D7-ahX" secondAttribute="centerY" id="8v6-O8-Rbm"/>
<constraint firstAttribute="centerX" secondItem="1H7-DG-oTV" secondAttribute="centerX" id="IUT-eC-9SL"/>
<constraint firstAttribute="centerY" secondItem="8M9-ZM-efS" secondAttribute="centerY" id="M2X-FT-Qkb"/>
<constraint firstAttribute="centerY" secondItem="1H7-DG-oTV" secondAttribute="centerY" id="DZN-Eh-ceZ"/>
<constraint firstAttribute="centerX" secondItem="5as-wy-D5h" secondAttribute="centerX" id="ExN-0x-heO"/>
<constraint firstItem="8M9-ZM-efS" firstAttribute="leading" secondItem="T3d-LT-ivg" secondAttribute="leading" id="Sw3-6S-Met"/>
<constraint firstItem="8M9-ZM-efS" firstAttribute="trailing" secondItem="T3d-LT-ivg" secondAttribute="trailing" id="YG7-1A-mES"/>
<constraint firstItem="8M9-ZM-efS" firstAttribute="top" secondItem="ZP5-e3-ge9" secondAttribute="top" constant="13" id="ZRr-1V-S6Z"/>
<constraint firstAttribute="trailing" secondItem="5as-wy-D5h" secondAttribute="trailing" id="Zfg-bu-N4E"/>
<constraint firstItem="5Dl-D7-ahX" firstAttribute="leading" secondItem="ZP5-e3-ge9" secondAttribute="leading" constant="10" id="bFJ-3g-wBh"/>
<constraint firstItem="8M9-ZM-efS" firstAttribute="leading" secondItem="5Dl-D7-ahX" secondAttribute="trailing" constant="10" id="hCS-JS-UW3"/>
<constraint firstItem="T3d-LT-ivg" firstAttribute="top" secondItem="8M9-ZM-efS" secondAttribute="bottom" constant="19" id="lBT-GX-5Pj"/>
<constraint firstAttribute="centerX" secondItem="1H7-DG-oTV" secondAttribute="centerX" id="oKx-SR-2E6"/>
<constraint firstAttribute="trailing" secondItem="8M9-ZM-efS" secondAttribute="trailing" constant="10" id="oL0-Kq-K48"/>
<constraint firstItem="5as-wy-D5h" firstAttribute="top" secondItem="ZP5-e3-ge9" secondAttribute="top" id="piP-Kc-0gl"/>
<constraint firstAttribute="centerY" secondItem="5as-wy-D5h" secondAttribute="centerY" id="uF3-qF-8KL"/>
<constraint firstItem="5as-wy-D5h" firstAttribute="leading" secondItem="ZP5-e3-ge9" secondAttribute="leading" id="zov-Qd-S57"/>
</constraints>
</view>
<prototypes>
@ -895,6 +1093,45 @@
<outlet property="settingSwitch" destination="l1h-g3-1Cr" id="trh-zz-dn2"/>
</connections>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="SettingsCellWithLabelAndTextField" rowHeight="120" id="bfI-Qq-oqO" customClass="SettingsCellWithLabelAndTextField">
<rect key="frame" x="140" y="28" width="320" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="bfI-Qq-oqO" id="l4g-kK-2cb">
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="If blank, all messages will trigger an alert.Your username &amp; display name always alerts." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="584" translatesAutoresizingMaskIntoConstraints="NO" id="5ty-Sv-0p1">
<rect key="frame" x="8" y="8" width="584" height="44"/>
<color key="tintColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="h8R-br-xLz"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Enter words separated by , (support regex)" minimumFontSize="14" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="am8-vZ-baW">
<rect key="frame" x="8" y="67" width="584" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="JGd-Xp-gSv"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="5ty-Sv-0p1" secondAttribute="trailing" id="9PC-6S-hN1"/>
<constraint firstItem="5ty-Sv-0p1" firstAttribute="leading" secondItem="l4g-kK-2cb" secondAttribute="leadingMargin" id="XTN-cr-Spa"/>
<constraint firstItem="5ty-Sv-0p1" firstAttribute="top" secondItem="l4g-kK-2cb" secondAttribute="topMargin" id="aUK-x5-GC1"/>
<constraint firstItem="am8-vZ-baW" firstAttribute="leading" secondItem="l4g-kK-2cb" secondAttribute="leadingMargin" id="gC5-aJ-Bas"/>
<constraint firstAttribute="trailingMargin" secondItem="am8-vZ-baW" secondAttribute="trailing" id="iRM-be-l7W"/>
<constraint firstAttribute="bottomMargin" secondItem="am8-vZ-baW" secondAttribute="bottom" id="jJZ-aQ-Ce2"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="settingLabel" destination="5ty-Sv-0p1" id="iWd-G2-b3e"/>
<outlet property="settingTextField" destination="am8-vZ-baW" id="Wnw-CQ-Pe5"/>
</connections>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="SettingsCellWithTextView" id="YFJ-c6-8tp" customClass="SettingsTableCellWithTextView">
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
@ -917,7 +1154,7 @@
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="settingTextView" destination="SAI-hV-Jai" id="UL1-hb-UbE"/>
<outlet property="settingTextView" destination="SAI-hV-Jai" id="7kp-Zv-MeL"/>
</connections>
</tableViewCell>
</prototypes>
@ -929,6 +1166,8 @@
<navigationItem key="navigationItem" title="Settings" id="7NM-zW-wJT"/>
<connections>
<outlet property="activityIndicator" destination="1H7-DG-oTV" id="UEC-5U-M70"/>
<outlet property="activityIndicatorBackgroundView" destination="5as-wy-D5h" id="UyJ-Fq-fhN"/>
<outlet property="saveUserInfoButton" destination="T3d-LT-ivg" id="IX1-WM-Dwv"/>
<outlet property="tableHeader" destination="ZP5-e3-ge9" id="nd0-lU-aW3"/>
<outlet property="tableView" destination="etG-ZU-b2r" id="5hz-jQ-qVT"/>
<outlet property="userDisplayName" destination="8M9-ZM-efS" id="rAQ-cX-3Ay"/>
@ -937,7 +1176,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ZKJ-22-Asy" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="875" y="841"/>
<point key="canvasLocation" x="875" y="840"/>
</scene>
<!--SettingsNav-->
<scene sceneID="b0q-mf-7ii">

View file

@ -164,7 +164,10 @@
- (void)dismiss:(BOOL)animated {
if ([_alert isKindOfClass:[UIAlertController class]]) {
[parentViewController dismissViewControllerAnimated:animated completion:nil];
// 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]]) {
@ -193,8 +196,11 @@
block(self);
});
}
// Release alert reference
_alert = nil;
dispatch_async(dispatch_get_main_queue(), ^{
// Release alert reference
_alert = nil;
});
}
#pragma mark - UIActionSheetDelegate (iOS < 8)
@ -208,8 +214,10 @@
block(self);
});
}
// Release _alert reference
_alert = nil;
dispatch_async(dispatch_get_main_queue(), ^{
// Release _alert reference
_alert = nil;
});
}
@end

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
@ -55,5 +55,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarHidden</key>
<false/>
</dict>
</plist>

View file

@ -16,8 +16,17 @@
#import <MatrixSDK/MatrixSDK.h>
#define MX_PREFIX_CONTENT_URI @"mxc://"
extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
typedef enum : NSUInteger {
MatrixHandlerStatusLoggedOut = 0,
MatrixHandlerStatusLogged,
MatrixHandlerStatusStoreDataReady,
MatrixHandlerStatusServerSyncDone
} MatrixHandlerStatus;
@interface MatrixHandler : NSObject
@property (strong, nonatomic) MXRestClient *mxRestClient;
@ -36,9 +45,12 @@ extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
// Matrix user's settings
@property (nonatomic) MXPresence userPresence;
@property (nonatomic,readonly) BOOL isLogged;
@property (nonatomic,readonly) BOOL isInitialSyncDone;
@property (nonatomic,readonly) MatrixHandlerStatus status;
@property (nonatomic,readonly) BOOL isResumeDone;
// return the MX cache size in bytes
@property (nonatomic,readonly) NSUInteger MXCacheSize;
// return the sum of the caches (MX cache + media cache ...)
@property (nonatomic,readonly) NSUInteger cachesSize;
+ (MatrixHandler *)sharedHandler;
@ -47,7 +59,7 @@ extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
- (void)logout;
// Flush and restore Matrix data
- (void)forceInitialSync;
- (void)forceInitialSync:(BOOL)clearCache;
- (void)enableInAppNotifications:(BOOL)isEnabled;
@ -61,4 +73,13 @@ extern NSString *const kMatrixHandlerUnsupportedMessagePrefix;
// search if a 1:1 conversation has been started with this member
- (NSString*) getRoomStartedWithMember:(MXRoomMember*)roomMember;
- (CGFloat)getPowerLevel:(MXRoomMember *)roomMember inRoom:(MXRoom *)room;
// provide a non empty display name
- (NSString*) getMXRoomMemberDisplayName:(MXRoomMember*)roomMember;
// return YES if the text contains a bing word
- (BOOL)containsBingWord:(NSString*)text;
@end

View file

@ -22,6 +22,8 @@
#import "MXFileStore.h"
#import "MXTools.h"
#import "MediaManager.h"
NSString *const kMatrixHandlerUnsupportedMessagePrefix = @"UNSUPPORTED MSG: ";
static MatrixHandler *sharedHandler = nil;
@ -36,7 +38,8 @@ static MatrixHandler *sharedHandler = nil;
id eventsListener;
}
@property (nonatomic,readwrite) BOOL isInitialSyncDone;
@property (strong, nonatomic) MXFileStore *mxFileStore;
@property (nonatomic,readwrite) MatrixHandlerStatus status;
@property (nonatomic,readwrite) BOOL isResumeDone;
@property (strong, nonatomic) CustomAlert *mxNotification;
@property (nonatomic) UIBackgroundTaskIdentifier bgTask;
@ -60,7 +63,7 @@ static MatrixHandler *sharedHandler = nil;
-(MatrixHandler *)init {
if (self = [super init]) {
_isInitialSyncDone = NO;
_status = (self.accessToken != nil) ? MatrixHandlerStatusLogged : MatrixHandlerStatusLoggedOut;
_isResumeDone = NO;
_userPresence = MXPresenceUnknown;
notifyOpenSessionFailure = YES;
@ -87,10 +90,10 @@ static MatrixHandler *sharedHandler = nil;
self.mxRestClient = [[MXRestClient alloc] initWithCredentials:credentials];
if (self.mxRestClient) {
// Use MXFileStore as MXStore to permanently store events
MXFileStore *store = [[MXFileStore alloc] init];
_mxFileStore = [[MXFileStore alloc] init];
[store openWithCredentials:credentials onComplete:^{
self.mxSession = [[MXSession alloc] initWithMatrixRestClient:self.mxRestClient andStore:store];
[_mxFileStore openWithCredentials:credentials onComplete:^{
self.mxSession = [[MXSession alloc] initWithMatrixRestClient:self.mxRestClient andStore:_mxFileStore];
// Check here whether the app user wants to display all the events
if ([[AppSettings sharedSettings] displayAllEvents]) {
// Use a filter to retrieve all the events (except kMXEventTypeStringPresence which are not related to a specific room)
@ -118,7 +121,9 @@ static MatrixHandler *sharedHandler = nil;
// Launch mxSession
[self.mxSession start:^{
self.isInitialSyncDone = YES;
self.status = MatrixHandlerStatusStoreDataReady;
} onServerSyncDone:^{
self.status = MatrixHandlerStatusServerSyncDone;
[self setUserPresence:MXPresenceOnline andStatusMessage:nil completion:nil];
_isResumeDone = YES;
@ -188,7 +193,6 @@ static MatrixHandler *sharedHandler = nil;
self.mxRestClient = nil;
}
self.isInitialSyncDone = NO;
_isResumeDone = NO;
notifyOpenSessionFailure = YES;
}
@ -215,12 +219,8 @@ static MatrixHandler *sharedHandler = nil;
#pragma mark -
- (BOOL)isLogged {
return (self.accessToken != nil);
}
- (void)pauseInBackgroundTask {
if (self.mxSession) {
if (self.mxSession && self.status == MatrixHandlerStatusServerSyncDone) {
_bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:_bgTask];
_bgTask = UIBackgroundTaskInvalid;
@ -244,7 +244,7 @@ static MatrixHandler *sharedHandler = nil;
}
- (void)resume {
if (self.mxSession && self.isInitialSyncDone) {
if (self.mxSession && self.status == MatrixHandlerStatusServerSyncDone) {
if (!self.isResumeDone) {
// Resume SDK and update user presence
[self.mxSession resume:^{
@ -272,10 +272,22 @@ static MatrixHandler *sharedHandler = nil;
// Keep userLogin, homeServerUrl
}
- (void)forceInitialSync {
if (self.isInitialSyncDone) {
- (void)forceInitialSync:(BOOL)clearCache {
if (self.status == MatrixHandlerStatusServerSyncDone || self.status == MatrixHandlerStatusStoreDataReady) {
self.status = MatrixHandlerStatusLogged;
[self closeSession];
notifyOpenSessionFailure = NO;
// Force back to Recents list if room details is displayed (Room details are not available until the end of initial sync)
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:NO];
if (clearCache) {
// clear the media cache
[MediaManager clearCache];
[_mxFileStore deleteAllData];
}
if (self.accessToken) {
[self openSession];
}
@ -297,30 +309,38 @@ static MatrixHandler *sharedHandler = nil;
[localNotification setAlertBody:[self displayTextForEvent:event withRoomState:roomState inSubtitleMode:YES]];
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
} else if (![event.userId isEqualToString:self.userId]
&& ![[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId]) {
// The sender is not the user and the concerned room is not presently visible,
// we display a notification by removing existing one (if any)
if (self.mxNotification) {
[self.mxNotification dismiss:NO];
&& ![[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId]
&& ![[AppDelegate theDelegate].masterTabBarController isPresentingMediaPicker]) {
NSString* messageText = [self displayTextForEvent:event withRoomState:roomState inSubtitleMode:YES];
// display the alert only the text contains an expected word
if ((0 == [AppSettings sharedSettings].specificWordsToAlertOn.count) ||[self containsBingWord:messageText]) {
// The sender is not the user and the concerned room is not presently visible,
// we display a notification by removing existing one (if any)
if (self.mxNotification) {
[self.mxNotification dismiss:NO];
}
self.mxNotification = [[CustomAlert alloc] initWithTitle:roomState.displayname
message:messageText
style:CustomAlertStyleAlert];
self.mxNotification.cancelButtonIndex = [self.mxNotification addActionWithTitle:@"OK"
style:CustomAlertActionStyleDefault
handler:^(CustomAlert *alert) {
[MatrixHandler sharedHandler].mxNotification = nil;
}];
[self.mxNotification addActionWithTitle:@"View"
style:CustomAlertActionStyleDefault
handler:^(CustomAlert *alert) {
[MatrixHandler sharedHandler].mxNotification = nil;
// Show the room
[[AppDelegate theDelegate].masterTabBarController showRoom:event.roomId];
}];
[self.mxNotification showInViewController:[[AppDelegate theDelegate].masterTabBarController selectedViewController]];
}
self.mxNotification = [[CustomAlert alloc] initWithTitle:roomState.displayname
message:[self displayTextForEvent:event withRoomState:roomState inSubtitleMode:YES]
style:CustomAlertStyleAlert];
self.mxNotification.cancelButtonIndex = [self.mxNotification addActionWithTitle:@"OK"
style:CustomAlertActionStyleDefault
handler:^(CustomAlert *alert) {
[MatrixHandler sharedHandler].mxNotification = nil;
}];
[self.mxNotification addActionWithTitle:@"View"
style:CustomAlertActionStyleDefault
handler:^(CustomAlert *alert) {
[MatrixHandler sharedHandler].mxNotification = nil;
// Show the room
[[AppDelegate theDelegate].masterTabBarController showRoom:event.roomId];
}];
[self.mxNotification showInViewController:[[AppDelegate theDelegate].masterTabBarController selectedViewController]];
}
}
}];
@ -417,9 +437,11 @@ static MatrixHandler *sharedHandler = nil;
if (inAccessToken.length) {
[[NSUserDefaults standardUserDefaults] setObject:inAccessToken forKey:@"accesstoken"];
[[AppDelegate theDelegate] registerUserNotificationSettings];
self.status = MatrixHandlerStatusLogged;
[self openSession];
} else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"accesstoken"];
self.status = MatrixHandlerStatusLoggedOut;
[self closeSession];
}
[[NSUserDefaults standardUserDefaults] synchronize];
@ -770,4 +792,89 @@ static MatrixHandler *sharedHandler = nil;
return nil;
}
- (NSUInteger) MXCacheSize {
if (self.mxFileStore) {
return self.mxFileStore.diskUsage;
}
return 0;
}
- (NSUInteger) cachesSize {
return self.MXCacheSize + [MediaManager cacheSize];
}
- (CGFloat)getPowerLevel:(MXRoomMember *)roomMember inRoom:(MXRoom *)room {
CGFloat powerLevel = 0;
// Customize banned and left (kicked) members
if (roomMember.membership == MXMembershipLeave || roomMember.membership == MXMembershipBan) {
powerLevel = 0;
} else {
// Handle power level display
//self.userPowerLevel.hidden = NO;
MXRoomPowerLevels *roomPowerLevels = room.state.powerLevels;
int maxLevel = 0;
for (NSString *powerLevel in roomPowerLevels.users.allValues) {
int level = [powerLevel intValue];
if (level > maxLevel) {
maxLevel = level;
}
}
NSUInteger userPowerLevel = [roomPowerLevels powerLevelOfUserWithUserID:roomMember.userId];
float userPowerLevelFloat = 0.0;
if (userPowerLevel) {
userPowerLevelFloat = userPowerLevel;
}
powerLevel = maxLevel ? userPowerLevelFloat / maxLevel : 1;
}
return powerLevel;
}
- (NSString*)getMXRoomMemberDisplayName:(MXRoomMember*)roomMember {
return roomMember.displayname.length == 0 ? roomMember.userId : roomMember.displayname;
}
// return YES if the text contains a bing word
- (BOOL)containsBingWord:(NSString*)text {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSMutableArray* wordsList = [[AppSettings sharedSettings].specificWordsToAlertOn mutableCopy];
// add the display name
if (mxHandler.mxSession.myUser.displayname.length) {
[wordsList addObject:mxHandler.mxSession.myUser.displayname];
}
// and the user identifiers
if (mxHandler.localPartFromUserId.length) {
[wordsList addObject:mxHandler.localPartFromUserId];
}
if (wordsList.count > 0) {
NSMutableString* pattern = [[NSMutableString alloc] init];
[pattern appendString:@"("];
for(NSString* word in wordsList) {
// check it is a regex
if ([pattern hasPrefix:@"\\b"] && [pattern hasSuffix:@"\\b"]) {
[pattern appendFormat:@"%@|", word];
} else {
[pattern appendFormat:@"\\b%@\\b|", word];
}
}
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"%@)", [pattern substringToIndex:pattern.length - 1]] options:NSRegularExpressionCaseInsensitive error:nil];
if ([regex numberOfMatchesInString:text options:0 range:NSMakeRange(0, [text length])]) {
return YES;
}
}
return NO;
}
@end

View file

@ -1,50 +0,0 @@
/*
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 <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
extern NSString *const kMediaManagerPrefixForDummyURL;
// The callback blocks
typedef void (^blockMediaManager_onImageReady)(UIImage *image);
typedef void (^blockMediaManager_onMediaReady)(NSString *cacheFilePath);
typedef void (^blockMediaManager_onError)(NSError *error);
@interface MediaManager : NSObject
+ (id)sharedInstance;
+ (UIImage *)resize:(UIImage *)image toFitInSize:(CGSize)size;
// Load a picture from the local cache or download it if it is not available yet.
// In this second case a mediaLoader reference is returned in order to let the user cancel this action.
+ (id)loadPicture:(NSString *)pictureURL
success:(blockMediaManager_onImageReady)success
failure:(blockMediaManager_onError)failure;
// Prepare a media from the local cache or download it if it is not available yet.
// In this second case a mediaLoader reference is returned in order to let the user cancel this action.
+ (id)prepareMedia:(NSString *)mediaURL
mimeType:(NSString *)mimeType
success:(blockMediaManager_onMediaReady)success
failure:(blockMediaManager_onError)failure;
+ (void)cancel:(id)mediaLoader;
+ (NSString *)cacheMediaData:(NSData *)mediaData forURL:(NSString *)mediaURL mimeType:(NSString *)mimeType;
+ (void)clearCache;
@end

View file

@ -1,327 +0,0 @@
/*
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 "MediaManager.h"
NSString *const kMediaManagerPrefixForDummyURL = @"dummyUrl-";
static NSString* mediaCachePath = nil;
static NSString *mediaDir = @"mediacache";
static MediaManager *sharedMediaManager = nil;
@interface MediaLoader : NSObject <NSURLConnectionDataDelegate> {
NSString *mediaURL;
NSString *mimeType;
blockMediaManager_onMediaReady onMediaReady;
blockMediaManager_onError onError;
NSMutableData *downloadData;
NSURLConnection *downloadConnection;
}
@end
#pragma mark - MediaLoader
@implementation MediaLoader
- (void)downloadPicture:(NSString*)pictureURL
success:(blockMediaManager_onImageReady)success
failure:(blockMediaManager_onError)failure {
// Download picture content
[self downloadMedia:pictureURL mimeType:@"image/jpeg" success:^(NSString *cacheFilePath) {
if (success) {
NSData* imageContent = [NSData dataWithContentsOfFile:cacheFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
if (imageContent) {
UIImage *image = [UIImage imageWithData:imageContent];
if (image) {
success(image);
} else {
NSLog(@"ERROR: picture download failed: %@", pictureURL);
if (failure){
failure(nil);
}
}
}
}
} failure:^(NSError *error) {
failure(error);
}];
}
- (void)downloadMedia:(NSString*)aMediaURL
mimeType:(NSString *)aMimeType
success:(blockMediaManager_onMediaReady)success
failure:(blockMediaManager_onError)failure {
// Report provided params
mediaURL = aMediaURL;
mimeType = aMimeType;
onMediaReady = success;
onError = failure;
// Start downloading
NSURL *url = [NSURL URLWithString:aMediaURL];
downloadData = [[NSMutableData alloc] init];
downloadConnection = [[NSURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
}
- (void)cancel {
// Reset blocks
onMediaReady = nil;
onError = nil;
// Cancel potential connection
if (downloadConnection) {
[downloadConnection cancel];
downloadConnection = nil;
downloadData = nil;
}
}
- (void)dealloc {
[self cancel];
}
#pragma mark -
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"ERROR: media download failed: %@, %@", error, mediaURL);
if (onError) {
onError (error);
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Append data
[downloadData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (downloadData.length) {
// Cache the downloaded data
NSString *cacheFilePath = [MediaManager cacheMediaData:downloadData forURL:mediaURL mimeType:mimeType];
// Call registered block
if (onMediaReady) {
onMediaReady(cacheFilePath);
}
} else {
NSLog(@"ERROR: media download failed: %@", mediaURL);
if (onError){
onError(nil);
}
}
downloadData = nil;
downloadConnection = nil;
}
@end
#pragma mark - MediaManager
@implementation MediaManager
+ (id)sharedInstance {
@synchronized(self) {
if(sharedMediaManager == nil)
sharedMediaManager = [[self alloc] init];
}
return sharedMediaManager;
}
+ (UIImage *)resize:(UIImage *)image toFitInSize:(CGSize)size {
UIImage *resizedImage = image;
// Check whether resize is required
if (size.width && size.height) {
CGFloat width = image.size.width;
CGFloat height = image.size.height;
if (width > size.width) {
height = (height * size.width) / width;
height = floorf(height / 2) * 2;
width = size.width;
}
if (height > size.height) {
width = (width * size.height) / height;
width = floorf(width / 2) * 2;
height = size.height;
}
if (width != image.size.width || height != image.size.height) {
// Create the thumbnail
CGSize imageSize = CGSizeMake(width, height);
UIGraphicsBeginImageContext(imageSize);
CGRect thumbnailRect = CGRectMake(0, 0, 0, 0);
thumbnailRect.origin = CGPointMake(0.0,0.0);
thumbnailRect.size.width = imageSize.width;
thumbnailRect.size.height = imageSize.height;
[image drawInRect:thumbnailRect];
resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
}
return resizedImage;
}
// Load a picture from the local cache or download it if it is not available yet.
// In this second case a mediaLoader reference is returned in order to let the user cancel this action.
+ (id)loadPicture:(NSString*)pictureURL
success:(blockMediaManager_onImageReady)success
failure:(blockMediaManager_onError)failure {
id ret = nil;
// Check cached pictures
UIImage *image = [MediaManager loadCachePicture:pictureURL];
if (image) {
if (success) {
// Reply synchronously
success (image);
}
}
else if ([pictureURL hasPrefix:kMediaManagerPrefixForDummyURL] == NO) {
// Create a media loader to download picture
MediaLoader *mediaLoader = [[MediaLoader alloc] init];
[mediaLoader downloadPicture:pictureURL success:success failure:failure];
ret = mediaLoader;
} else {
NSLog(@"Load tmp picture from cache failed: %@", pictureURL);
if (failure){
failure(nil);
}
}
return ret;
}
+ (id)prepareMedia:(NSString *)mediaURL
mimeType:(NSString *)mimeType
success:(blockMediaManager_onMediaReady)success
failure:(blockMediaManager_onError)failure {
id ret = nil;
// Check cache
NSString* filename = [MediaManager getCacheFileNameFor:mediaURL mimeType:mimeType];
if ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
if (success) {
// Reply synchronously
success (filename);
}
}
else if ([mediaURL hasPrefix:kMediaManagerPrefixForDummyURL] == NO) {
// Create a media loader to download media content
MediaLoader *mediaLoader = [[MediaLoader alloc] init];
[mediaLoader downloadMedia:mediaURL mimeType:mimeType success:success failure:failure];
ret = mediaLoader;
} else {
NSLog(@"Load tmp media from cache failed: %@", mediaURL);
if (failure){
failure(nil);
}
}
return ret;
}
+ (void)cancel:(id)mediaLoader {
[((MediaLoader*)mediaLoader) cancel];
}
+ (NSString*)cacheMediaData:(NSData*)mediaData forURL:(NSString *)mediaURL mimeType:(NSString *)mimeType {
NSString* filename = [MediaManager getCacheFileNameFor:mediaURL mimeType:mimeType];
if ([mediaData writeToFile:filename atomically:YES]) {
return filename;
} else {
return nil;
}
}
+ (void)clearCache {
NSError *error = nil;
if (!mediaCachePath) {
// compute the path
mediaCachePath = [MediaManager getCachePath];
}
if (mediaCachePath) {
if (![[NSFileManager defaultManager] removeItemAtPath:mediaCachePath error:&error]) {
NSLog(@"Fails to delete media cache dir : %@", error);
} else {
NSLog(@"Media cache : deleted !");
}
} else {
NSLog(@"Media cache does not exist");
}
mediaCachePath = nil;
}
#pragma mark - Cache handling
+ (UIImage*)loadCachePicture:(NSString*)pictureURL {
UIImage* res = nil;
NSString* filename = [MediaManager getCacheFileNameFor:pictureURL mimeType:@"image/jpeg"];
if ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
NSData* imageContent = [NSData dataWithContentsOfFile:filename options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
if (imageContent) {
res = [[UIImage alloc] initWithData:imageContent];
}
}
return res;
}
+ (NSString*)getCachePath {
NSString *cachePath = nil;
if (!mediaCachePath) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheRoot = [paths objectAtIndex:0];
mediaCachePath = [cacheRoot stringByAppendingPathComponent:mediaDir];
if (![[NSFileManager defaultManager] fileExistsAtPath:mediaCachePath]) {
[[NSFileManager defaultManager] createDirectoryAtPath:mediaCachePath withIntermediateDirectories:NO attributes:nil error:nil];
}
}
cachePath = mediaCachePath;
return cachePath;
}
+ (NSString*)getCacheFileNameFor:(NSString*)mediaURL mimeType:(NSString *)mimeType {
NSString *fileName;
if ([mimeType isEqualToString:@"image/jpeg"]) {
fileName = [NSString stringWithFormat:@"ima%lu.jpg", (unsigned long)mediaURL.hash];
} else if ([mimeType isEqualToString:@"video/mp4"]) {
fileName = [NSString stringWithFormat:@"video%lu.mp4", (unsigned long)mediaURL.hash];
} else if ([mimeType isEqualToString:@"video/quicktime"]) {
fileName = [NSString stringWithFormat:@"video%lu.mov", (unsigned long)mediaURL.hash];
} else {
NSString *extension = @"";
NSArray *components = [mediaURL componentsSeparatedByString:@"."];
if (components && components.count > 1) {
extension = [components lastObject];
}
fileName = [NSString stringWithFormat:@"%lu.%@", (unsigned long)mediaURL.hash, extension];
}
return [[MediaManager getCachePath] stringByAppendingPathComponent:fileName];
}
@end

View file

@ -15,6 +15,11 @@
*/
#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;

View file

@ -17,9 +17,12 @@
#import "RecentRoom.h"
#import "MatrixHandler.h"
NSString *const kRecentRoomUpdatedByBackPagination = @"kRecentRoomUpdatedByBackPagination";
@interface RecentRoom() {
MXRoom *mxRoom;
id backPaginationListener;
NSOperation *backPaginationOperation;
}
@end
@ -40,9 +43,9 @@
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]) {
// get back one more event
[self triggerBackPagination];
if ([self updateWithLastEvent:event andRoomState:roomState markAsUnread:NO]) {
// Force recents refresh
[[NSNotificationCenter defaultCenter] postNotificationName:kRecentRoomUpdatedByBackPagination object:nil];
}
}
}];
@ -83,8 +86,14 @@
- (void)triggerBackPagination {
if (mxRoom.canPaginate) {
[mxRoom paginateBackMessages:1 complete:^{
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];
}];
@ -99,6 +108,10 @@
backPaginationListener = nil;
mxRoom = nil;
}
if (backPaginationOperation) {
[backPaginationOperation cancel];
backPaginationOperation = nil;
}
}
@end

View file

@ -20,6 +20,9 @@
#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,
@ -59,6 +62,14 @@ typedef enum : NSUInteger {
@property (nonatomic) NSDictionary *attachmentInfo;
@property (nonatomic) NSString *thumbnailURL;
@property (nonatomic) NSDictionary *thumbnailInfo;
@property (nonatomic) NSString *previewURL;
@property (nonatomic) NSString *uploadId;
@property (nonatomic) CGFloat uploadProgress;
// Patch: Outgoing messages may be received from events stream whereas the app is waiting for our PUT to return.
// In this case, some messages are temporary hidden
// The following property is true when all components are hidden
@property (nonatomic, readonly) BOOL isHidden;
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState;
@ -68,7 +79,8 @@ typedef enum : NSUInteger {
// Remove the item defined with this event id
// Return false if the event id is not found
- (BOOL)removeEvent:(NSString*)eventId;
// 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;

View file

@ -19,6 +19,9 @@
#import "MatrixHandler.h"
#import "AppSettings.h"
NSString *const kRoomMessageLocalPreviewKey = @"kRoomMessageLocalPreviewKey";
NSString *const kRoomMessageUploadIdKey = @"kRoomMessageUploadIdKey";
static NSAttributedString *messageSeparator = nil;
@interface RoomMessage() {
@ -33,6 +36,7 @@ static NSAttributedString *messageSeparator = nil;
@end
@implementation RoomMessage
@synthesize uploadProgress;
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState {
if (self = [super init]) {
@ -43,6 +47,7 @@ static NSAttributedString *messageSeparator = nil;
_senderAvatarUrl = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
_maxTextViewWidth = ROOM_MESSAGE_DEFAULT_MAX_TEXTVIEW_WIDTH;
_contentSize = CGSizeZero;
self.uploadProgress = -1;
currentAttributedTextMsg = nil;
// Set message type (consider text by default), and check attachment if any
@ -52,19 +57,34 @@ static NSAttributedString *messageSeparator = nil;
NSString *msgtype = event.content[@"msgtype"];
if ([msgtype isEqualToString:kMXMessageTypeImage]) {
_messageType = RoomMessageTypeImage;
// Retrieve content url/info
_attachmentURL = event.content[@"url"];
_attachmentInfo = event.content[@"info"];
// Handle thumbnail url/info
_thumbnailURL = event.content[@"thumbnail_url"];
_thumbnailInfo = event.content[@"thumbnail_info"];
if (!_thumbnailURL) {
if ([_attachmentURL hasPrefix:MX_PREFIX_CONTENT_URI]) {
// Build the url to get the well adapted thumbnail from server
_thumbnailURL = _attachmentURL;
NSString *mxThumbnailPrefix = [NSString stringWithFormat:@"%@%@/thumbnail/", [mxHandler homeServerURL], kMXMediaPathPrefix];
_thumbnailURL = [_thumbnailURL stringByReplacingOccurrencesOfString:MX_PREFIX_CONTENT_URI withString:mxThumbnailPrefix];
// Add parameters
_thumbnailURL = [NSString stringWithFormat:@"%@?width=%tu&height=%tu&method=scale", _thumbnailURL, (NSUInteger)self.contentSize.width, (NSUInteger)self.contentSize.height];
} else {
_thumbnailURL = _attachmentURL;
}
}
} 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"];
}
@ -72,6 +92,10 @@ static NSAttributedString *messageSeparator = nil;
// 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
@ -144,13 +168,17 @@ static NSAttributedString *messageSeparator = nil;
return NO;
}
- (BOOL)containsEventId:(NSString *)eventId {
- (RoomMessageComponent*)componentWithEventId:(NSString *)eventId {
for (RoomMessageComponent* msgComponent in messageComponents) {
if ([msgComponent.eventId isEqualToString:eventId]) {
return YES;
return msgComponent;
}
}
return NO;
return nil;
}
- (BOOL)containsEventId:(NSString *)eventId {
return nil != [self componentWithEventId:eventId];
}
- (void)hideComponent:(BOOL)isHidden withEventId:(NSString*)eventId {
@ -222,9 +250,15 @@ static NSAttributedString *messageSeparator = nil;
} else if (_messageType == RoomMessageTypeImage || _messageType == RoomMessageTypeVideo) {
CGFloat width, height;
width = height = 40;
if (_thumbnailInfo) {
width = [_thumbnailInfo[@"w"] integerValue];
height = [_thumbnailInfo[@"h"] integerValue];
if (_thumbnailInfo || _attachmentInfo) {
if (_thumbnailInfo) {
width = [_thumbnailInfo[@"w"] integerValue];
height = [_thumbnailInfo[@"h"] integerValue];
} else {
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;
@ -302,6 +336,16 @@ static NSAttributedString *messageSeparator = nil;
return NO;
}
- (BOOL)isHidden {
if (_messageType == RoomMessageTypeText) {
return (!self.attributedTextMessage.length);
} else if (messageComponents.count) {
RoomMessageComponent *msgComponent = [messageComponents firstObject];
return msgComponent.isHidden;
}
return YES;
}
#pragma mark -
- (void)sortComponents {

View file

@ -17,7 +17,7 @@
#import <MatrixSDK/MatrixSDK.h>
extern NSString *const kLocalEchoEventIdPrefix;
extern NSString *const kFailedEventId;
extern NSString *const kFailedEventIdPrefix;
typedef enum : NSUInteger {
RoomMessageComponentStyleDefault,

View file

@ -18,7 +18,7 @@
#import "MatrixHandler.h"
NSString *const kLocalEchoEventIdPrefix = @"localEcho-";
NSString *const kFailedEventId = @"failedEventId";
NSString *const kFailedEventIdPrefix = @"failedEventId-";
@implementation RoomMessageComponent
@ -51,9 +51,9 @@ NSString *const kFailedEventId = @"failedEventId";
BOOL isIncomingMsg = ([event.userId isEqualToString:mxHandler.userId] == NO);
if ([textMessage hasPrefix:kMatrixHandlerUnsupportedMessagePrefix]) {
_style = RoomMessageComponentStyleUnsupported;
} else if ([_eventId hasPrefix:kFailedEventId]) {
} else if ([_eventId hasPrefix:kFailedEventIdPrefix]) {
_style = RoomMessageComponentStyleFailed;
} else if (isIncomingMsg && !_isStateEvent && [self containsBingWord]) {
} else if (isIncomingMsg && !_isStateEvent && [mxHandler containsBingWord:_textMessage]) {
_style = RoomMessageComponentStyleBing;
} else if (!isIncomingMsg && [_eventId hasPrefix:kLocalEchoEventIdPrefix]) {
_style = RoomMessageComponentStyleInProgress;
@ -68,29 +68,6 @@ NSString *const kFailedEventId = @"failedEventId";
return self;
}
- (BOOL)containsBingWord {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSString *pattern = nil;
if (mxHandler.mxSession.myUser.displayname.length) {
pattern = [NSString stringWithFormat:@"\\b%@\\b", mxHandler.mxSession.myUser.displayname];
}
if (mxHandler.localPartFromUserId.length) {
if (pattern) {
pattern = [NSString stringWithFormat:@"(%@|\\b%@\\b)", pattern, mxHandler.localPartFromUserId];
} else {
pattern = [NSString stringWithFormat:@"\\b%@\\b", mxHandler.localPartFromUserId];
}
}
if (pattern) {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
if ([regex numberOfMatchesInString:_textMessage options:0 range:NSMakeRange(0, [_textMessage length])]) {
return YES;
}
}
return NO;
}
- (NSDictionary*)stringAttributes {
UIColor *textColor;
UIFont *font;

View file

@ -0,0 +1,40 @@
/*
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>
// Customize UIImageView in order to let UIImageView handle automatically remote url
@interface CustomImageView : UIImageView
typedef void (^blockCustomImageView_onClick)(CustomImageView *imageView, NSString* title);
@property (strong, nonatomic) NSString *placeholder;
@property (strong, nonatomic) NSString *imageURL;
// Use this boolean to hide activity indicator during image downloading
@property (nonatomic) BOOL hideActivityIndicator;
// Information about the media represented by this image (image, video...)
@property (strong, nonatomic) NSDictionary *mediaInfo;
// Let the user defines some custom buttons over the tabbar
- (void)setLeftButtonTitle :leftButtonTitle handler:(blockCustomImageView_onClick)handler;
- (void)setRightButtonTitle:rightButtonTitle handler:(blockCustomImageView_onClick)handler;
- (void)dismissSelection;
@end

View file

@ -0,0 +1,244 @@
/*
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 "CustomImageView.h"
#import "MediaManager.h"
#import "AppDelegate.h"
@interface CustomImageView () {
id imageLoader;
UIActivityIndicatorView *loadingWheel;
// validation buttons
UIButton* leftButton;
UIButton* rightButton;
NSString* leftButtonTitle;
NSString* rightButtonTitle;
blockCustomImageView_onClick leftHandler;
blockCustomImageView_onClick rightHandler;
UIView* bottomBarView;
}
@end
@implementation CustomImageView
#define CUSTOM_IMAGE_VIEW_BUTTON_WIDTH 100
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
leftButtonTitle = nil;
leftHandler = nil;
rightButtonTitle = nil;
rightHandler = nil;
self.backgroundColor = [UIColor blackColor];
self.contentMode = UIViewContentModeScaleAspectFit;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
}
return self;
}
- (void)dealloc {
if (imageLoader) {
[MediaManager cancel:imageLoader];
imageLoader = nil;
}
if (loadingWheel) {
[loadingWheel removeFromSuperview];
loadingWheel = nil;
}
if (bottomBarView) {
[bottomBarView removeFromSuperview];
bottomBarView = nil;
}
}
- (void)startActivityIndicator {
// Add activity indicator if none
if (loadingWheel == nil) {
loadingWheel = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self addSubview:loadingWheel];
}
// Adjust position
CGPoint center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
loadingWheel.center = center;
// Adjust color
if ([self.backgroundColor isEqual:[UIColor blackColor]]) {
loadingWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
} else {
loadingWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}
// Start
[loadingWheel startAnimating];
}
- (void)stopActivityIndicator {
if (loadingWheel) {
[loadingWheel stopAnimating];
}
}
#pragma mark -
- (IBAction)onButtonToggle:(id)sender
{
if (sender == leftButton) {
dispatch_async(dispatch_get_main_queue(), ^{
leftHandler(self, leftButtonTitle);
});
} else if (sender == rightButton) {
dispatch_async(dispatch_get_main_queue(), ^{
rightHandler(self, rightButtonTitle);
});
}
}
// add a generic button to the bottom view
// return the added UIButton
- (UIButton*) addbuttonWithTitle:(NSString*)title {
UIButton* button = [[UIButton alloc] init];
[button setTitle:title forState:UIControlStateNormal];
[button setTitle:title forState:UIControlStateHighlighted];
// default background text color
CGFloat textColorFactor = 146.0 / 255.0;
UIColor* textColor = [UIColor colorWithRed:textColorFactor green:textColorFactor blue:textColorFactor alpha:1.0];
[button setTitleColor:textColor forState:UIControlStateNormal];
[button setTitleColor:textColor forState:UIControlStateHighlighted];
// keep the bottomView background color
button.backgroundColor = [UIColor clearColor];
[button addTarget:self action:@selector(onButtonToggle:) forControlEvents:UIControlEventTouchUpInside];
[bottomBarView addSubview:button];
return button;
}
- (void)layoutSubviews {
// call upper layer
[super layoutSubviews];
// check if the dedicated buttons are already added
if (leftButtonTitle || rightButtonTitle) {
if (!bottomBarView) {
bottomBarView = [[UIView alloc] init];
if (leftButtonTitle) {
leftButton = [self addbuttonWithTitle:leftButtonTitle];
}
rightButton = [[UIButton alloc] init];
if (rightButtonTitle) {
rightButton = [self addbuttonWithTitle:rightButtonTitle];
}
// default tabbar background color
CGFloat base = 248.0 / 255.0f;
bottomBarView.backgroundColor = [UIColor colorWithRed:base green:base blue:base alpha:1.0];
[[AppDelegate theDelegate].masterTabBarController.tabBar addSubview:bottomBarView];
}
// manage the item
CGRect tabBarFrame = [AppDelegate theDelegate].masterTabBarController.tabBar.frame;
tabBarFrame.origin.y = 0;
bottomBarView.frame = tabBarFrame;
if (leftButton) {
leftButton.frame = CGRectMake(0, 0, CUSTOM_IMAGE_VIEW_BUTTON_WIDTH, bottomBarView.frame.size.height);
}
if (rightButton) {
rightButton.frame = CGRectMake(bottomBarView.frame.size.width - CUSTOM_IMAGE_VIEW_BUTTON_WIDTH, 0, CUSTOM_IMAGE_VIEW_BUTTON_WIDTH, bottomBarView.frame.size.height);
}
}
}
- (void)setHideActivityIndicator:(BOOL)hideActivityIndicator {
_hideActivityIndicator = hideActivityIndicator;
if (hideActivityIndicator) {
[self stopActivityIndicator];
} else if (imageLoader) {
// Loading is in progress, start activity indicator
[self startActivityIndicator];
}
}
- (void)setImageURL:(NSString *)imageURL {
// Cancel media loader in progress (if any)
if (imageLoader) {
[MediaManager cancel:imageLoader];
imageLoader = nil;
}
_imageURL = imageURL;
// Reset image view
self.image = nil;
if (_placeholder) {
// Set picture placeholder
self.image = [UIImage imageNamed:_placeholder];
}
// Consider provided url to update image view
if (imageURL) {
// Load picture
if (!_hideActivityIndicator) {
[self startActivityIndicator];
}
imageLoader = [MediaManager loadPicture:imageURL
success:^(UIImage *image) {
[self stopActivityIndicator];
self.image = image;
}
failure:^(NSError *error) {
[self stopActivityIndicator];
NSLog(@"Failed to download image (%@): %@", imageURL, error);
}];
}
}
#pragma mark - buttons management
- (void)setLeftButtonTitle: aLeftButtonTitle handler:(blockCustomImageView_onClick)handler {
leftButtonTitle = aLeftButtonTitle;
leftHandler = handler;
}
- (void)setRightButtonTitle:aRightButtonTitle handler:(blockCustomImageView_onClick)handler {
rightButtonTitle = aRightButtonTitle;
rightHandler = handler;
}
- (void)dismissSelection {
if (bottomBarView) {
[bottomBarView removeFromSuperview];
bottomBarView = nil;
}
}
@end

View file

@ -16,11 +16,12 @@
#import <UIKit/UIKit.h>
// Customize UIImageView in order to let UIImageView handle automatically remote url
@interface CustomImageView : UIImageView
// Customize UIView in order to display image defined with remote url. Zooming inside the image (Stretching) is supported.
@interface CustomImageView : UIView <UIScrollViewDelegate>
@property (strong, nonatomic) NSString *placeholder;
@property (strong, nonatomic) NSString *imageURL;
typedef void (^blockCustomImageView_onClick)(CustomImageView *imageView, NSString* title);
- (void)setImageURL:(NSString *)imageURL withPreviewImage:(UIImage*)previewImage;
// Use this boolean to hide activity indicator during image downloading
@property (nonatomic) BOOL hideActivityIndicator;
@ -28,5 +29,16 @@
// Information about the media represented by this image (image, video...)
@property (strong, nonatomic) NSDictionary *mediaInfo;
@property (strong, nonatomic) UIImage *image;
@property (nonatomic) BOOL stretchable;
@property (nonatomic) BOOL fullScreen;
// Let the user defines some custom buttons over the tabbar
- (void)setLeftButtonTitle :leftButtonTitle handler:(blockCustomImageView_onClick)handler;
- (void)setRightButtonTitle:rightButtonTitle handler:(blockCustomImageView_onClick)handler;
- (void)dismissSelection;
@end

View file

@ -16,94 +16,542 @@
#import "CustomImageView.h"
#import "MediaManager.h"
#import "AppDelegate.h"
#import "PieChartView.h"
@interface CustomImageView () {
id imageLoader;
UIActivityIndicatorView *loadingWheel;
NSString *imageURL;
UIImage *currentImage;
// the loading view is composed with the spinner and a pie chart
// the spinner is display until progress > 0
UIView *loadingView;
UIActivityIndicatorView *waitingDownloadSpinner;
PieChartView *pieChartView;
UILabel *progressInfoLabel;
// validation buttons
UIButton *leftButton;
UIButton *rightButton;
NSString *leftButtonTitle;
NSString *rightButtonTitle;
blockCustomImageView_onClick leftHandler;
blockCustomImageView_onClick rightHandler;
UIView* bottomBarView;
// Subviews
UIScrollView *scrollView;
UIImageView *imageView;
BOOL useFullScreen;
}
@end
@implementation CustomImageView
@synthesize stretchable;
#define CUSTOM_IMAGE_VIEW_BUTTON_WIDTH 100
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
leftButtonTitle = nil;
leftHandler = nil;
rightButtonTitle = nil;
rightHandler = nil;
self.backgroundColor = [UIColor blackColor];
self.contentMode = UIViewContentModeScaleAspectFit;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
}
return self;
}
- (void)dealloc {
if (imageLoader) {
[MediaManager cancel:imageLoader];
imageLoader = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self stopActivityIndicator];
if (loadingView) {
[loadingView removeFromSuperview];
loadingView = nil;
}
if (loadingWheel) {
[loadingWheel removeFromSuperview];
loadingWheel = nil;
if (bottomBarView) {
[bottomBarView removeFromSuperview];
bottomBarView = nil;
}
}
- (void)startActivityIndicator {
// Add activity indicator if none
if (loadingWheel == nil) {
loadingWheel = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self addSubview:loadingWheel];
// create the views if they don't exist
if (!waitingDownloadSpinner) {
waitingDownloadSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
CGRect frame = waitingDownloadSpinner.frame;
frame.size.width += 30;
frame.size.height += 30;
waitingDownloadSpinner.bounds = frame;
[waitingDownloadSpinner.layer setCornerRadius:5];
}
// Adjust position
CGPoint center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
loadingWheel.center = center;
if (!loadingView) {
loadingView = [[UIView alloc] init];
loadingView.frame = waitingDownloadSpinner.bounds;
waitingDownloadSpinner.frame = waitingDownloadSpinner.bounds;
[loadingView addSubview:waitingDownloadSpinner];
loadingView.backgroundColor = [UIColor clearColor];
[self addSubview:loadingView];
}
if (!pieChartView) {
pieChartView = [[PieChartView alloc] init];
pieChartView.frame = loadingView.bounds;
pieChartView.progress = 0;
pieChartView.progressColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.25];
pieChartView.unprogressColor = [UIColor clearColor];
[loadingView addSubview:pieChartView];
}
// display the download statistics
if (useFullScreen && !progressInfoLabel) {
progressInfoLabel = [[UILabel alloc] init];
progressInfoLabel.backgroundColor = [UIColor whiteColor];
progressInfoLabel.textColor = [UIColor blackColor];
progressInfoLabel.font = [UIFont systemFontOfSize:8];
progressInfoLabel.alpha = 0.25;
progressInfoLabel.text = @"";
progressInfoLabel.numberOfLines = 0;
[progressInfoLabel sizeToFit];
[self addSubview:progressInfoLabel];
}
// initvalue
loadingView.hidden = NO;
pieChartView.progress = 0;
// Adjust color
if ([self.backgroundColor isEqual:[UIColor blackColor]]) {
loadingWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
waitingDownloadSpinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
// a preview image could be displayed
// ensure that the white spinner is visible
// it could be drawn on a white area
waitingDownloadSpinner.backgroundColor = [UIColor darkGrayColor];
} else {
loadingWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
waitingDownloadSpinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
}
// ensure that the spinner is drawn at the top
[loadingView.superview bringSubviewToFront:loadingView];
// Adjust position
CGPoint center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
loadingView.center = center;
// Start
[loadingWheel startAnimating];
[waitingDownloadSpinner startAnimating];
}
- (void)stopActivityIndicator {
if (loadingWheel) {
[loadingWheel stopAnimating];
if (waitingDownloadSpinner && waitingDownloadSpinner.isAnimating) {
[waitingDownloadSpinner stopAnimating];
}
pieChartView.progress = 0;
loadingView.hidden = YES;
if (progressInfoLabel) {
[progressInfoLabel removeFromSuperview];
progressInfoLabel = nil;
}
}
#pragma mark - setters/getters
- (void)setImage:(UIImage *)anImage {
currentImage = anImage;
imageView.image = anImage;
[self initScrollZoomFactors];
}
- (UIImage*)image {
return currentImage;
}
- (void)setFullScreen:(BOOL)fullScreen {
useFullScreen = fullScreen;
[self initLayout];
if (useFullScreen) {
[self removeFromSuperview];
[UIApplication sharedApplication].statusBarHidden = YES;
self.frame = [AppDelegate theDelegate].window.rootViewController.view.bounds;
[[AppDelegate theDelegate].window.rootViewController.view addSubview:self];
}
}
- (BOOL)fullScreen {
return useFullScreen;
}
#pragma mark -
- (IBAction)onButtonToggle:(id)sender
{
if (sender == leftButton) {
dispatch_async(dispatch_get_main_queue(), ^{
leftHandler(self, leftButtonTitle);
});
} else if (sender == rightButton) {
dispatch_async(dispatch_get_main_queue(), ^{
rightHandler(self, rightButtonTitle);
});
}
}
// add a generic button to the bottom view
// return the added UIButton
- (UIButton*) addbuttonWithTitle:(NSString*)title {
UIButton* button = [[UIButton alloc] init];
[button setTitle:title forState:UIControlStateNormal];
[button setTitle:title forState:UIControlStateHighlighted];
if (useFullScreen) {
// use the same text color as the tabbar
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
}
else {
// use the same text color as the tabbar
[button setTitleColor:[AppDelegate theDelegate].masterTabBarController.tabBar.tintColor forState:UIControlStateNormal];
[button setTitleColor:[AppDelegate theDelegate].masterTabBarController.tabBar.tintColor forState:UIControlStateHighlighted];
}
// keep the bottomView background color
button.backgroundColor = [UIColor clearColor];
[button addTarget:self action:@selector(onButtonToggle:) forControlEvents:UIControlEventTouchUpInside];
[bottomBarView addSubview:button];
return button;
}
- (void)initScrollZoomFactors {
// check if the image can be zoomed
if (self.image && self.stretchable && imageView.frame.size.width && imageView.frame.size.height) {
// ensure that the content size is properly initialized
scrollView.contentSize = scrollView.frame.size;
// compute the appliable zoom factor
// assume that the user does not expect to zoom more than 100%
CGSize imageSize = self.image.size;
CGFloat scaleX = imageSize.width / imageView.frame.size.width;
CGFloat scaleY = imageSize.height / imageView.frame.size.height;
if (scaleX < scaleY)
{
scaleX = scaleY;
}
if (scaleX < 1.0)
{
scaleX = 1.0;
}
scrollView.zoomScale = 1.0;
scrollView.minimumZoomScale = 1.0;
scrollView.maximumZoomScale = scaleX;
// update the image frame to ensure that it fits to the scrollview frame
imageView.frame = scrollView.bounds;
}
}
- (void)removeFromSuperview {
[super removeFromSuperview];
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (useFullScreen) {
[UIApplication sharedApplication].statusBarHidden = NO;
}
[self stopActivityIndicator];
}
- (void)initLayout {
// create the subviews if they don't exist
if (!scrollView) {
scrollView = [[UIScrollView alloc] init];
scrollView.delegate = self;
scrollView.backgroundColor = [UIColor clearColor];
[self addSubview:scrollView];
imageView = [[UIImageView alloc] init];
imageView.backgroundColor = [UIColor clearColor];
imageView.contentMode = self.contentMode;
[scrollView addSubview:imageView];
}
}
- (void)layoutSubviews {
// call upper layer
[super layoutSubviews];
[self initLayout];
// the image has been updated
if (imageView.image != self.image) {
imageView.image = self.image;
}
CGRect tabBarFrame = [AppDelegate theDelegate].masterTabBarController.tabBar.frame;
// update the scrollview frame
CGRect oneSelfFrame = self.frame;
CGRect scrollViewFrame = CGRectIntegral(scrollView.frame);
if (leftButtonTitle || rightButtonTitle) {
oneSelfFrame.size.height -= tabBarFrame.size.height;
}
oneSelfFrame = CGRectIntegral(oneSelfFrame);
oneSelfFrame.origin = scrollViewFrame.origin = CGPointZero;
// use integral rect to avoid rounded value issue (float precision)
if (!CGRectEqualToRect(oneSelfFrame, scrollViewFrame)) {
scrollView.frame = oneSelfFrame;
imageView.frame = oneSelfFrame;
[self initScrollZoomFactors];
}
// check if the dedicated buttons are already added
if (leftButtonTitle || rightButtonTitle) {
if (!bottomBarView) {
bottomBarView = [[UIView alloc] init];
if (leftButtonTitle) {
leftButton = [self addbuttonWithTitle:leftButtonTitle];
}
rightButton = [[UIButton alloc] init];
if (rightButtonTitle) {
rightButton = [self addbuttonWithTitle:rightButtonTitle];
}
// in fullscreen, display both buttons above the view
if (useFullScreen) {
bottomBarView.backgroundColor = [UIColor blackColor];
[self addSubview:bottomBarView];
}
// display them above the tabbar
else {
// default tabbar background color
CGFloat base = 248.0 / 255.0f;
bottomBarView.backgroundColor = [UIColor colorWithRed:base green:base blue:base alpha:1.0];
[[AppDelegate theDelegate].masterTabBarController.tabBar addSubview:bottomBarView];
}
}
if (useFullScreen) {
tabBarFrame.origin.y = self.frame.size.height - tabBarFrame.size.height;
}
else {
tabBarFrame.origin.y = 0;
}
bottomBarView.frame = tabBarFrame;
if (leftButton) {
leftButton.frame = CGRectMake(0, 0, CUSTOM_IMAGE_VIEW_BUTTON_WIDTH, bottomBarView.frame.size.height);
}
if (rightButton) {
rightButton.frame = CGRectMake(bottomBarView.frame.size.width - CUSTOM_IMAGE_VIEW_BUTTON_WIDTH, 0, CUSTOM_IMAGE_VIEW_BUTTON_WIDTH, bottomBarView.frame.size.height);
}
}
if (!loadingView.hidden) {
// ensure that the spinner is drawn at the top
[loadingView.superview bringSubviewToFront:loadingView];
// Adjust positions
CGPoint center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
loadingView.center = center;
CGRect progressInfoLabelFrame = progressInfoLabel.frame;
progressInfoLabelFrame.origin.x = center.x - (progressInfoLabelFrame.size.width / 2);
progressInfoLabelFrame.origin.y = 10 + loadingView.frame.origin.y + loadingView.frame.size.height;
progressInfoLabel.frame = progressInfoLabelFrame;
}
}
- (void)setHideActivityIndicator:(BOOL)hideActivityIndicator {
_hideActivityIndicator = hideActivityIndicator;
if (hideActivityIndicator) {
[self stopActivityIndicator];
} else if (imageLoader) {
} else if ([MediaManager existingDownloaderForURL:imageURL]) {
// Loading is in progress, start activity indicator
[self startActivityIndicator];
}
}
- (void)setImageURL:(NSString *)imageURL {
// Cancel media loader in progress (if any)
if (imageLoader) {
[MediaManager cancel:imageLoader];
imageLoader = nil;
- (void)setImageURL:(NSString *)anImageURL withPreviewImage:(UIImage*)previewImage {
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
imageURL = anImageURL;
if (!imageURL) {
// Set preview by default
self.image = previewImage;
return;
}
_imageURL = imageURL;
// Reset image view
self.image = nil;
if (_placeholder) {
// Set picture placeholder
self.image = [UIImage imageNamed:_placeholder];
}
// Consider provided url to update image view
if (imageURL) {
// Load picture
if (!_hideActivityIndicator) {
[self startActivityIndicator];
// Check whether the image download is in progress
MediaLoader* loader = [MediaManager existingDownloaderForURL:imageURL];
if (loader) {
// Set preview until the image is loaded
self.image = previewImage;
// update the progress UI with the current info
[self updateProgressUI:loader.statisticsDict];
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadProgress:) name:kMediaDownloadProgressNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
} else {
// Retrieve the image from cache
UIImage* image = [MediaManager loadCachePictureForURL:imageURL];
if (image) {
self.image = image;
[self stopActivityIndicator];
} else {
// Set preview until the image is loaded
self.image = previewImage;
// Trigger image downloading
if (!_hideActivityIndicator) {
[self startActivityIndicator];
}
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadProgress:) name:kMediaDownloadProgressNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
[MediaManager downloadMediaFromURL:imageURL withType:@"image/jpeg"];
}
imageLoader = [MediaManager loadPicture:imageURL
success:^(UIImage *image) {
[self stopActivityIndicator];
self.image = image;
}
failure:^(NSError *error) {
[self stopActivityIndicator];
NSLog(@"Failed to download image (%@): %@", imageURL, error);
}];
}
}
- (void)onMediaDownloadEnd:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:imageURL]) {
[self stopActivityIndicator];
// update the image
UIImage* image = [MediaManager loadCachePictureForURL:imageURL];
if (image) {
self.image = image;
}
// remove the observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
}
}
- (void)updateProgressUI:(NSDictionary*)downloadStatsDict {
NSNumber* progressNumber = [downloadStatsDict valueForKey:kMediaLoaderProgressRateKey];
if (progressNumber) {
pieChartView.progress = progressNumber.floatValue;
waitingDownloadSpinner.hidden = YES;
}
if (progressInfoLabel) {
NSString* downloadRate = [downloadStatsDict valueForKey:kMediaLoaderProgressDownloadRateKey];
NSString* remaingTime = [downloadStatsDict valueForKey:kMediaLoaderProgressRemaingTimeKey];
NSString* progressString = [downloadStatsDict valueForKey:kMediaLoaderProgressStringKey];
NSMutableString* text = [[NSMutableString alloc] init];
[text appendString:progressString];
if (remaingTime) {
[text appendFormat:@" (%@)", remaingTime];
}
if (downloadRate) {
[text appendFormat:@"\n %@", downloadRate];
}
progressInfoLabel.text = text;
// on multilines, sizeToFit uses the current width
// so reset it
progressInfoLabel.frame = CGRectZero;
[progressInfoLabel sizeToFit];
//
CGRect progressInfoLabelFrame = progressInfoLabel.frame;
progressInfoLabelFrame.origin.x = self.center.x - (progressInfoLabelFrame.size.width / 2);
progressInfoLabelFrame.origin.y = 10 + loadingView.frame.origin.y + loadingView.frame.size.height;
progressInfoLabel.frame = progressInfoLabelFrame;
}
}
- (void)onMediaDownloadProgress:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:imageURL]) {
[self updateProgressUI:notif.userInfo];
}
}
}
#pragma mark - buttons management
- (void)setLeftButtonTitle: aLeftButtonTitle handler:(blockCustomImageView_onClick)handler {
leftButtonTitle = aLeftButtonTitle;
leftHandler = handler;
}
- (void)setRightButtonTitle:aRightButtonTitle handler:(blockCustomImageView_onClick)handler {
rightButtonTitle = aRightButtonTitle;
rightHandler = handler;
}
- (void)dismissSelection {
if (bottomBarView) {
[bottomBarView removeFromSuperview];
bottomBarView = nil;
}
if (useFullScreen) {
[UIApplication sharedApplication].statusBarHidden = NO;
}
}
#pragma mark - UIScrollViewDelegate
// require to be able to zoom an image
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.stretchable ? imageView : nil;
}
@end

View file

@ -0,0 +1,27 @@
/*
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 MemberActionsCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UIButton *rightButton;
@property (weak, nonatomic) IBOutlet UIButton *leftButton;
-(void) setLeftButtonText:(NSString*)text;
-(void) setRightButtonText:(NSString*)text;
@end

View file

@ -0,0 +1,42 @@
/*
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 "MemberActionsCell.h"
@implementation MemberActionsCell
- (void)initButton:(UIButton*)button withText:(NSString*)text {
button.hidden = (text.length == 0);
button.layer.borderColor = [UIColor blackColor].CGColor;
button.layer.borderWidth = 1;
button.layer.cornerRadius = 5;
[button setTitle:text forState:UIControlStateNormal];
[button setTitle:text forState:UIControlStateHighlighted];
}
- (void) setLeftButtonText:(NSString*)text {
[self initButton:self.leftButton withText:text];
}
- (void) setRightButtonText:(NSString*)text {
[self initButton:self.rightButton withText:text];
}
@end

View file

@ -0,0 +1,28 @@
/*
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 PieChartView : UIView
// 0 -> 1
@property (nonatomic) CGFloat progress;
@property (strong, nonatomic) UIColor* progressColor;
@property (strong, nonatomic) UIColor* unprogressColor;
@end

View file

@ -0,0 +1,141 @@
/*
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 "PieChartView.h"
@interface PieChartView () {
// graphical items
CAShapeLayer* backgroundContainerLayer;
CAShapeLayer* powerContainerLayer;
CGFloat _progress;
UIColor* _progressColor;
UIColor* _unprogressColor;
}
@end
@implementation PieChartView
- (void)setProgress:(CGFloat)progress {
_progress = progress;
// no power level -> hide the pie
if (0 >= progress) {
self.hidden = YES;
return;
}
// ensure that the progress value does not excceed 1.0
progress = MIN(progress, 1.0);
// display it
self.hidden = NO;
// defines the view settings
CGFloat radius = self.frame.size.width / 2;
// draw a rounded view
[self.layer setCornerRadius:radius];
self.backgroundColor = [UIColor clearColor];
// draw the pie
CALayer* layer = [self layer];
// remove any previous drawn layer
if (powerContainerLayer) {
[powerContainerLayer removeFromSuperlayer];
}
// define default colors
if (!_progressColor) {
_progressColor = [UIColor redColor];
}
if (!_unprogressColor) {
_unprogressColor = [UIColor lightGrayColor];
}
// the background cell color is hidden the cell is selected.
// so put in grey the cell background triggers a weird display (the background grey is hidden but not the red part).
// add an other layer fixes the UX.
if (!backgroundContainerLayer) {
backgroundContainerLayer = [CAShapeLayer layer];
[backgroundContainerLayer setZPosition:0];
[backgroundContainerLayer setStrokeColor:NULL];
backgroundContainerLayer.fillColor = _unprogressColor.CGColor;
// build the path
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, radius, radius);
CGPathAddArc(path, NULL, radius, radius, radius, 0 , 2 * M_PI, 0);
CGPathCloseSubpath(path);
[backgroundContainerLayer setPath:path];
CFRelease(path);
// add the sub layer
[layer addSublayer:backgroundContainerLayer];
}
// create the red layer
powerContainerLayer = [CAShapeLayer layer];
[powerContainerLayer setZPosition:0];
[powerContainerLayer setStrokeColor:NULL];
// power level is drawn in red
powerContainerLayer.fillColor = _progressColor.CGColor;
// build the path
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, radius, radius);
CGPathAddArc(path, NULL, radius, radius, radius, -M_PI / 2, (progress * 2 * M_PI) - (M_PI / 2), 0);
CGPathCloseSubpath(path);
[powerContainerLayer setPath:path];
CFRelease(path);
// add the sub layer
[layer addSublayer:powerContainerLayer];
}
- (CGFloat) progress {
return _progress;
}
- (void)setProgressColor:(UIColor *)progressColor {
_progressColor = progressColor;
self.progress = _progress;
}
- (UIColor*) progressColor {
return _progressColor;
}
- (void)setUnprogressColor:(UIColor *)unprogressColor {
_unprogressColor = unprogressColor;
self.progress = _progress;
}
- (UIColor*) unprogressColor {
return _unprogressColor;
}
@end

View file

@ -16,15 +16,14 @@
#import <UIKit/UIKit.h>
#import "CustomImageView.h"
#import "PieChartView.h"
@class MXRoomMember;
@class MXRoom;
// Room Member Table View Cell
@interface RoomMemberTableCell : UITableViewCell
{
//
CAShapeLayer* powerContainerLayer;
@interface RoomMemberTableCell : UITableViewCell {
PieChartView* pieChartView;
}
@property (strong, nonatomic) IBOutlet CustomImageView *pictureView;
@property (weak, nonatomic) IBOutlet UILabel *userLabel;

View file

@ -89,44 +89,14 @@
// display it
self.powerContainer.hidden = NO;
// defines the view settings
CGFloat radius = self.powerContainer.frame.size.width / 2;
self.powerContainer.backgroundColor = [UIColor clearColor];
// draw a rounded view
[self.powerContainer.layer setCornerRadius:radius];
// the default body color is gray
self.powerContainer.backgroundColor = [UIColor lightGrayColor];
// draw the pie
CALayer* layer = [self.powerContainer layer];
// remove any previous drawn layer
if (powerContainerLayer) {
[powerContainerLayer removeFromSuperlayer];
if (!pieChartView) {
pieChartView = [[PieChartView alloc] initWithFrame:self.powerContainer.bounds];
[self.powerContainer addSubview:pieChartView];
}
// create the red layer
powerContainerLayer = [CAShapeLayer layer];
[powerContainerLayer setZPosition:0];
[powerContainerLayer setStrokeColor:NULL];
// power level is drawn in red
powerContainerLayer.fillColor = [UIColor redColor].CGColor;
// build the path
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, radius, radius);
CGPathAddArc(path, NULL, radius, radius, radius, -M_PI / 2, (progress * 2 * M_PI) - (M_PI / 2), 0);
CGPathCloseSubpath(path);
[powerContainerLayer setPath:path];
CFRelease(path);
// add the sub layer
[layer addSublayer:powerContainerLayer];
pieChartView.progress = progress;
}
- (void)setRoomMember:(MXRoomMember *)roomMember withRoom:(MXRoom *)room {
@ -134,9 +104,8 @@
// set the user info
self.userLabel.text = [room.state memberName:roomMember.userId];
// user
self.pictureView.placeholder = @"default-profile";
self.pictureView.imageURL = roomMember.avatarUrl;
// user thumbnail
[self.pictureView setImageURL:roomMember.avatarUrl withPreviewImage:[UIImage imageNamed:@"default-profile"]];
// Round image view
[self.pictureView.layer setCornerRadius:self.pictureView.frame.size.width / 2];
@ -165,24 +134,7 @@
} else {
self.backgroundColor = [UIColor whiteColor];
// Handle power level display
//self.userPowerLevel.hidden = NO;
MXRoomPowerLevels *roomPowerLevels = room.state.powerLevels;
int maxLevel = 0;
for (NSString *powerLevel in roomPowerLevels.users.allValues) {
int level = [powerLevel intValue];
if (level > maxLevel) {
maxLevel = level;
}
}
NSUInteger userPowerLevel = [roomPowerLevels powerLevelOfUserWithUserID:roomMember.userId];
float userPowerLevelFloat = 0.0;
if (userPowerLevel) {
userPowerLevelFloat = userPowerLevel;
}
powerLevel = maxLevel ? userPowerLevelFloat / maxLevel : 1;
powerLevel = [[MatrixHandler sharedHandler] getPowerLevel:roomMember inRoom:room];
// get the user presence and his thumbnail border color
if (roomMember.membership == MXMembershipInvite) {

View file

@ -17,6 +17,7 @@
#import <UIKit/UIKit.h>
#import "CustomImageView.h"
#import "RoomMessage.h"
#import "PieChartView.h"
// Room Message Table View Cell
@interface RoomMessageTableCell : UITableViewCell
@ -25,6 +26,11 @@
@property (strong, nonatomic) IBOutlet CustomImageView *attachmentView;
@property (strong, nonatomic) IBOutlet UIImageView *playIconView;
@property (weak, nonatomic) IBOutlet UIView *dateTimeLabelContainer;
@property (weak, nonatomic) IBOutlet UIView *progressView;
@property (weak, nonatomic) IBOutlet UILabel *statsLabel;
@property (weak, nonatomic) IBOutlet PieChartView *progressChartView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *msgTextViewTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachViewWidthConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachViewTopConstraint;
@ -32,6 +38,11 @@
// reference to the linked message
@property (strong, nonatomic) RoomMessage *message;
- (void)startProgressUI;
- (void)stopProgressUI;
- (void)cancelDownload;
@end
@interface IncomingMessageTableCell : RoomMessageTableCell
@ -39,6 +50,8 @@
@end
@interface OutgoingMessageTableCell : RoomMessageTableCell
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
-(void)startUploadAnimating;
-(void)stopAnimating;
@end

View file

@ -16,16 +16,178 @@
#import "RoomMessageTableCell.h"
#import "MediaManager.h"
#import "PieChartView.h"
@implementation RoomMessageTableCell
- (void)dealloc {
// remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)updateProgressUI:(NSDictionary*)statisticsDict {
self.progressView.hidden = NO;
NSString* downloadRate = [statisticsDict valueForKey:kMediaLoaderProgressDownloadRateKey];
NSString* remaingTime = [statisticsDict valueForKey:kMediaLoaderProgressRemaingTimeKey];
NSString* progressString = [statisticsDict valueForKey:kMediaLoaderProgressStringKey];
NSMutableString* text = [[NSMutableString alloc] init];
if (progressString) {
[text appendString:progressString];
}
if (downloadRate) {
[text appendFormat:@"\n%@", downloadRate];
}
if (remaingTime) {
[text appendFormat:@"\n%@", remaingTime];
}
self.statsLabel.text = text;
NSNumber* progressNumber = [statisticsDict valueForKey:kMediaLoaderProgressRateKey];
if (progressNumber) {
self.progressChartView.progress = progressNumber.floatValue;
}
}
- (void)onMediaDownloadProgress:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:self.message.attachmentURL]) {
[self updateProgressUI:notif.userInfo];
}
}
}
- (void)onMediaDownloadEnd:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:self.message.attachmentURL]) {
[self stopProgressUI];
// the job is really over
if ([notif.name isEqualToString:kMediaDownloadDidFinishNotification]) {
// remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
}
}
}
- (void)startProgressUI {
BOOL isHidden = YES;
// remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
// there is an attachment URL
if (self.message.attachmentURL) {
// check if there is a downlad in progress
MediaLoader *loader = [MediaManager existingDownloaderForURL:self.message.attachmentURL];
NSDictionary *dict = loader.statisticsDict;
if (dict) {
isHidden = NO;
// defines the text to display
[self updateProgressUI:dict];
}
// anyway listen to the progress event
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadProgress:) name:kMediaDownloadProgressNotification object:nil];
}
self.progressView.hidden = isHidden;
}
- (void)stopProgressUI {
self.progressView.hidden = YES;
// do not remove the observer here
// the download could restart without recomposing the cell
}
- (void)cancelDownload {
// get the linked medida loader
MediaLoader *loader = [MediaManager existingDownloaderForURL:self.message.attachmentURL];
if (loader) {
[loader cancel];
}
// ensure there is no more progress bar
[self stopProgressUI];
}
@end
@implementation IncomingMessageTableCell
@end
@interface OutgoingMessageTableCell () {
}
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@end
@implementation OutgoingMessageTableCell
- (void)dealloc {
[self stopAnimating];
}
-(void)startUploadAnimating {
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaUploadProgressNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUploadProgress:) name:kMediaUploadProgressNotification object:nil];
self.activityIndicator.hidden = NO;
[self.activityIndicator startAnimating];
MediaLoader *uploader = [MediaManager existingUploaderWithId:self.message.uploadId];
if (uploader && uploader.statisticsDict) {
self.activityIndicator.hidden = YES;
[self updateProgressUI:uploader.statisticsDict];
} else {
self.activityIndicator.hidden = NO;
self.progressView.hidden = YES;
}
}
-(void)stopAnimating {
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMediaUploadProgressNotification object:nil];
[self.activityIndicator stopAnimating];
}
- (void)onUploadProgress:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString *uploadId = notif.object;
if ([uploadId isEqualToString:self.message.uploadId]) {
self.activityIndicator.hidden = YES;
[self updateProgressUI:notif.userInfo];
// the upload is ended
if (self.progressChartView.progress == 1.0) {
self.progressView.hidden = YES;
}
}
}
}
- (void)layoutSubviews {
[super layoutSubviews];

View file

@ -18,7 +18,7 @@
@class MXRoom;
@interface RoomTitleView : UIView {
@interface RoomTitleView : UIView<UIGestureRecognizerDelegate> {
}
@property (weak, nonatomic) IBOutlet UITextField *displayNameTextField;
@ -30,4 +30,10 @@
- (void)dismissKeyboard;
// force to refresh the title display
- (void)refreshDisplay;
// return YES if the animation has been stopped
- (BOOL)stopTopicAnimation;
@end

View file

@ -19,6 +19,14 @@
@interface RoomTitleView () {
id messagesListener;
// the topic can be animated if it is longer than the screen size
UIScrollView* scrollView;
UILabel* label;
UIView* topicTextFieldMaskView;
// do not start the topic animation asap
NSTimer * animationTimer;
}
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *displayNameTextFieldTopConstraint;
@end
@ -31,12 +39,16 @@
messagesListener = nil;
}
_mxRoom = nil;
// stop any animation
[self stopTopicAnimation];
}
- (void)refreshDisplay {
if (_mxRoom) {
_displayNameTextField.text = _mxRoom.state.displayname;
_topicTextField.text = _mxRoom.state.topic;
// replace empty string by nil : avoid having the placeholder 'Room name" when there is no displayname
_displayNameTextField.text = (_mxRoom.state.displayname.length) ? _mxRoom.state.displayname : nil;
_topicTextField.text = (_mxRoom.state.topic) ? _mxRoom.state.topic : nil;
} else {
_displayNameTextField.text = nil;
_topicTextField.text = nil;
@ -76,6 +88,7 @@
}
- (void)setHiddenTopic:(BOOL)hiddenTopic {
[self stopTopicAnimation];
if (hiddenTopic) {
_topicTextField.hidden = YES;
_displayNameTextFieldTopConstraint.constant = 10;
@ -85,10 +98,173 @@
}
}
// start with delay
- (void)startTopicAnimation {
// stop any pending timer
if (animationTimer) {
[animationTimer invalidate];
animationTimer = nil;
}
// already animated the topic
if (scrollView) {
return;
}
// compute the text width
UIFont* font = _topicTextField.font;
// see font description
if (!font) {
font = [UIFont systemFontOfSize:12];
}
NSDictionary *attributes = @{NSFontAttributeName: font};
CGSize stringSize = CGSizeMake(CGFLOAT_MAX, _topicTextField.frame.size.height);
stringSize = [_topicTextField.text boundingRectWithSize:stringSize
options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
attributes:attributes
context:nil].size;
// does not need to animate the text
if (stringSize.width < _topicTextField.frame.size.width) {
return;
}
// put the text in a scrollView to animat it
scrollView = [[UIScrollView alloc] initWithFrame: _topicTextField.frame];
label = [[UILabel alloc] initWithFrame:_topicTextField.frame];
label.text = _topicTextField.text;
label.textColor = _topicTextField.textColor;
label.font = _topicTextField.font;
// move to the top left
CGRect topicTextFieldFrame = _topicTextField.frame;
topicTextFieldFrame.origin = CGPointZero;
label.frame = topicTextFieldFrame;
_topicTextField.hidden = YES;
[scrollView addSubview:label];
[self insertSubview:scrollView belowSubview:topicTextFieldMaskView];
// update the size
[label sizeToFit];
// offset
CGPoint offset = scrollView.contentOffset;
offset.x = label.frame.size.width - scrollView.frame.size.width;
// duration (magic computation to give more time if the text is longer)
CGFloat duration = label.frame.size.width / scrollView.frame.size.width * 3;
// animate the topic once to display its full content
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionCurveLinear animations:^{
[scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
[self stopTopicAnimation];
}];
}
- (BOOL)stopTopicAnimation {
// stop running timers
if (animationTimer) {
[animationTimer invalidate];
animationTimer = nil;
}
// if there is an animation is progress
if (scrollView) {
_topicTextField.hidden = NO;
[scrollView.layer removeAllAnimations];
[scrollView removeFromSuperview];
scrollView = nil;
label = nil;
[self addSubview:_topicTextField];
// must be done to be able to restart the animation
// the Z order is not kept
[self bringSubviewToFront:topicTextFieldMaskView];
return YES;
}
return NO;
}
- (void)editTopic {
[self stopTopicAnimation];
dispatch_async(dispatch_get_main_queue(), ^{
[_topicTextField becomeFirstResponder];
});
}
- (void)dismissKeyboard {
// Hide the keyboard
[_displayNameTextField resignFirstResponder];
[_topicTextField resignFirstResponder];
// restart the animation
[self stopTopicAnimation];
}
- (void)layoutSubviews {
// mother class call
[super layoutSubviews];
// add a mask to trap the tap events
// it is faster (and simpliest) than subclassing the scrollview or the textField
// any other gesture could also be trapped here
if (!topicTextFieldMaskView) {
topicTextFieldMaskView = [[UIView alloc] initWithFrame:_topicTextField.frame];
topicTextFieldMaskView.backgroundColor = [UIColor clearColor];
[self addSubview:topicTextFieldMaskView];
// tap -> switch to text edition
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(editTopic)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[topicTextFieldMaskView addGestureRecognizer:tap];
// long tap -> animate the topic
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(startTopicAnimation)];
[topicTextFieldMaskView addGestureRecognizer:longPress];
}
}
- (void)setFrame:(CGRect)frame {
// mother class call
[super setFrame:frame];
// stop any running animation if the frame is updated (screen rotation for example)
if (!CGRectEqualToRect(CGRectIntegral(frame), CGRectIntegral(self.frame))) {
// stop any running application
[self stopTopicAnimation];
}
// update the mask frame
if (self.topicTextField.hidden) {
topicTextFieldMaskView.frame = CGRectZero;
} else {
topicTextFieldMaskView.frame = self.topicTextField.frame;
}
// topicTextField switches becomes the first responder or it is not anymore the first responder
if (self.topicTextField.isFirstResponder != (topicTextFieldMaskView.hidden)) {
topicTextFieldMaskView.hidden = self.topicTextField.isFirstResponder;
// move topicTextFieldMaskView to the foreground
// when topicTextField has been the first responder, it lets a view over topicTextFieldMaskView
// so restore the expected Z order
if (!topicTextFieldMaskView.hidden) {
[self bringSubviewToFront:topicTextFieldMaskView];
}
}
}
@end

View file

@ -26,4 +26,9 @@
@interface SettingsTableCellWithTextView : SettingsTableViewCell
@property (strong, nonatomic) IBOutlet UITextView *settingTextView;
@end
@interface SettingsCellWithLabelAndTextField : SettingsTableViewCell
@property (strong, nonatomic) IBOutlet UILabel *settingLabel;
@property (strong, nonatomic) IBOutlet UITextField *settingTextField;
@end

View file

@ -23,4 +23,7 @@
@end
@implementation SettingsTableCellWithTextView
@end
@implementation SettingsCellWithLabelAndTextField
@end

View file

@ -30,6 +30,8 @@
NSMutableArray *filteredPublicRooms;
BOOL searchBarShouldEndEditing;
UIView *savedTableHeaderView;
NSString *homeServerSuffix;
}
@property (weak, nonatomic) IBOutlet UITableView *publicRoomsTable;
@ -81,9 +83,10 @@
// Ensure to display room creation section
[self.tableView scrollRectToVisible:_roomCreationLabel.frame animated:NO];
if ([[MatrixHandler sharedHandler] isLogged]) {
if ([MatrixHandler sharedHandler].status != MatrixHandlerStatusLoggedOut) {
homeServerSuffix = [NSString stringWithFormat:@":%@",[MatrixHandler sharedHandler].homeServer];
// Update alias placeholder
_roomAliasTextField.placeholder = [NSString stringWithFormat:@"(e.g. #foo:%@)", [MatrixHandler sharedHandler].homeServer];
_roomAliasTextField.placeholder = [NSString stringWithFormat:@"(e.g. #foo%@)", homeServerSuffix];
// Refresh listed public rooms
[self refreshPublicRooms];
}
@ -160,9 +163,13 @@
// Remove '#' character
alias = [alias substringFromIndex:1];
// Remove homeserver
NSString *suffix = [NSString stringWithFormat:@":%@",[MatrixHandler sharedHandler].homeServer];
NSRange range = [alias rangeOfString:suffix];
alias = [alias stringByReplacingCharactersInRange:range withString:@""];
NSRange range = [alias rangeOfString:homeServerSuffix];
if (range.location == NSNotFound) {
NSLog(@"Wrong room alias has been set (%@)", _roomAliasTextField.text);
alias = nil;
} else {
alias = [alias stringByReplacingCharactersInRange:range withString:@""];
}
}
if (! alias.length) {
@ -211,10 +218,7 @@
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
if (textField == _roomAliasTextField) {
textField.text = self.alias;
textField.placeholder = @"foo";
} else if (textField == _participantsTextField) {
if (textField == _participantsTextField) {
if (textField.text.length == 0) {
textField.text = @"@";
}
@ -223,14 +227,17 @@
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField == _roomAliasTextField) {
// Compute the new alias with this string change
NSString * alias = textField.text;
if (alias.length) {
// add homeserver as suffix
textField.text = [NSString stringWithFormat:@"#%@:%@", alias, [MatrixHandler sharedHandler].homeServer];
// Check whether homeserver suffix should be added
NSRange range = [textField.text rangeOfString:@":"];
if (range.location == NSNotFound) {
textField.text = [textField.text stringByAppendingString:homeServerSuffix];
}
// Check whether the alias is valid
if (!self.alias) {
// reset text field
textField.text = nil;
[self onTextFieldChange:nil];
}
textField.placeholder = [NSString stringWithFormat:@"(e.g. #foo:%@)", [MatrixHandler sharedHandler].homeServer];
} else if (textField == _participantsTextField) {
NSArray *participants = self.participantsList;
textField.text = [participants componentsJoinedByString:@"; "];
@ -242,23 +249,41 @@
if (textField == _participantsTextField) {
// Auto completion is active only when the change concerns the end of the current string
if (range.location == textField.text.length) {
NSString *participants = [textField.text stringByReplacingCharactersInRange:range withString:string];
if ([string isEqualToString:@";"]) {
// Add '@' character
participants = [participants stringByAppendingString:@" @"];
textField.text = [textField.text stringByAppendingString:@"; @"];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
} else if ([string isEqualToString:@":"]) {
// Add homeserver
if ([MatrixHandler sharedHandler].homeServer) {
participants = [participants stringByAppendingString:[MatrixHandler sharedHandler].homeServer];
}
textField.text = [textField.text stringByAppendingString:homeServerSuffix];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
}
} else if (textField == _roomAliasTextField) {
// Add # if none
if (!textField.text.length) {
if ([string isEqualToString:@"#"] == NO) {
if ([string isEqualToString:@":"]) {
textField.text = [NSString stringWithFormat:@"#%@",homeServerSuffix];
} else {
textField.text = [NSString stringWithFormat:@"#%@",string];
}
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
} else {
// Add homeserver automatically when user adds ':' at the end
if (range.location == textField.text.length && [string isEqualToString:@":"]) {
textField.text = [textField.text stringByAppendingString:homeServerSuffix];
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
textField.text = participants;
// Update Create button status
[self onTextFieldChange:nil];
return NO;
}
}
return YES;
@ -318,29 +343,6 @@
}
}
#pragma mark - scrollView delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
// hide the keyboard if the user scrolls the public rooms list
if (!filteredPublicRooms) {
if ([self.roomNameTextField isFirstResponder]) {
[self.roomNameTextField resignFirstResponder];
[self.tableView becomeFirstResponder];
}
if ([self.roomAliasTextField isFirstResponder]) {
[self.roomNameTextField resignFirstResponder];
[self.tableView becomeFirstResponder];
}
if ([self.participantsTextField isFirstResponder]) {
[self.participantsTextField resignFirstResponder];
[self.tableView becomeFirstResponder];
}
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

View file

@ -29,6 +29,7 @@
- (void)popRoomViewControllerAnimated:(BOOL)animated;
- (BOOL)isPresentingMediaPicker;
- (void)presentMediaPicker:(UIImagePickerController*)mediaPicker;
- (void)dismissMediaPicker;

View file

@ -60,7 +60,7 @@
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (! [[MatrixHandler sharedHandler] isLogged]) {
if ([MatrixHandler sharedHandler].status == MatrixHandlerStatusLoggedOut) {
[self showLoginScreen];
}
}
@ -124,6 +124,10 @@
}
}
- (BOOL)isPresentingMediaPicker {
return nil != mediaPicker;
}
- (void)presentMediaPicker:(UIImagePickerController*)aMediaPicker {
[self dismissMediaPicker];
[self presentViewController:aMediaPicker animated:YES completion:^{

View file

@ -0,0 +1,27 @@
/*
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 "MatrixHandler.h"
@interface MemberViewController : UITableViewController
@property (strong, nonatomic) MXRoomMember *mxRoomMember;
@property (strong, nonatomic) MXRoom *mxRoom;
@end

View file

@ -0,0 +1,538 @@
/*
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 "MemberViewController.h"
#import "AppDelegate.h"
#import "MemberActionsCell.h"
#import "MediaManager.h"
@interface MemberViewController () {
MediaLoader* imageLoader;
id membersListener;
NSMutableArray* buttonsTitles;
// mask view while processing a request
UIView* pendingRequestMask;
UIActivityIndicatorView * pendingMaskSpinnerView;
}
// graphical objects
@property (strong, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UIButton *memberThumbnailButton;
@property (weak, nonatomic) IBOutlet UITextView *roomMemberMID;
@property (strong, nonatomic) CustomAlert *actionMenu;
- (IBAction)onButtonToggle:(id)sender;
@end
@implementation MemberViewController
@synthesize mxRoom;
- (void)dealloc {
// close any pending actionsheet
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (imageLoader) {
[imageLoader cancel];
imageLoader = nil;
}
if (membersListener) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxSession removeListener:membersListener];
membersListener = nil;
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// remove the line separator color
self.tableView.separatorColor = [UIColor clearColor];
self.tableView.rowHeight = 44;
self.tableView.allowsSelection = NO;
buttonsTitles = [[NSMutableArray alloc] init];
// ignore useless update
if (_mxRoomMember) {
[self updateMemberInfo];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSArray *mxMembersEvents = @[
kMXEventTypeStringRoomMember,
kMXEventTypeStringRoomPowerLevels
];
// list on member updates
membersListener = [mxHandler.mxSession listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) {
// consider only live event
if (direction == MXEventDirectionForwards) {
// Check the room Id (if any)
if (event.roomId && [event.roomId isEqualToString:mxRoom.state.roomId] == NO) {
// This event does not concern the current room members
return;
}
// Hide potential action sheet
if (self.actionMenu) {
[self.actionMenu dismiss:NO];
self.actionMenu = nil;
}
MXRoomMember* nextRoomMember = nil;
// get the updated memmber
NSArray* membersList = [self.mxRoom.state members];
for (MXRoomMember* member in membersList) {
if ([member.userId isEqual:_mxRoomMember.userId]) {
nextRoomMember = member;
break;
}
}
// does the member still exist ?
if (nextRoomMember) {
// Refresh members list
_mxRoomMember = nextRoomMember;
[self updateMemberInfo];
[self.tableView reloadData];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController popToRootViewControllerAnimated:NO];
[[AppDelegate theDelegate].masterTabBarController setVisibleRoomId:nil];
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:YES];
});
}
}
}];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (imageLoader) {
[imageLoader cancel];
imageLoader = nil;
}
if (membersListener) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxSession removeListener:membersListener];
membersListener = nil;
}
}
- (void)updateMemberInfo {
self.title = _mxRoomMember.displayname ? _mxRoomMember.displayname : _mxRoomMember.userId;
// set the thumbnail info
[[self.memberThumbnailButton imageView] setContentMode: UIViewContentModeScaleAspectFill];
[[self.memberThumbnailButton imageView] setClipsToBounds:YES];
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (_mxRoomMember.avatarUrl) {
// Check whether the image download is in progress
id loader = [MediaManager existingDownloaderForURL:_mxRoomMember.avatarUrl];
if (loader) {
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
} else {
// Retrieve the image from cache
UIImage* image = [MediaManager loadCachePictureForURL:_mxRoomMember.avatarUrl];
if (image) {
[self.memberThumbnailButton setImage:image forState:UIControlStateNormal];
[self.memberThumbnailButton setImage:image forState:UIControlStateHighlighted];
} else {
// Cancel potential download in progress
if (imageLoader) {
[imageLoader cancel];
}
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
imageLoader = [MediaManager downloadMediaFromURL:_mxRoomMember.avatarUrl withType:@"image/jpeg"];
}
}
} else {
UIImage *image = [UIImage imageNamed:@"default-profile"];
if (image) {
[self.memberThumbnailButton setImage:image forState:UIControlStateNormal];
[self.memberThumbnailButton setImage:image forState:UIControlStateHighlighted];
}
}
self.roomMemberMID.text = _mxRoomMember.userId;
}
- (void)onMediaDownloadEnd:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:_mxRoomMember.avatarUrl]) {
// update the image
UIImage* image = [MediaManager loadCachePictureForURL:_mxRoomMember.avatarUrl];
if (image == nil) {
image = [UIImage imageNamed:@"default-profile"];
}
if (image) {
[self.memberThumbnailButton setImage:image forState:UIControlStateNormal];
[self.memberThumbnailButton setImage:image forState:UIControlStateHighlighted];
}
// remove the observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
imageLoader = nil;
}
}
}
- (void)setRoomMember:(MXRoomMember*) aRoomMember {
// ignore useless update
if (![_mxRoomMember.userId isEqualToString:aRoomMember.userId]) {
_mxRoomMember = aRoomMember;
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
// Check user's power level before allowing an action (kick, ban, ...)
MXRoomPowerLevels *powerLevels = [mxRoom.state powerLevels];
NSUInteger memberPowerLevel = [powerLevels powerLevelOfUserWithUserID:_mxRoomMember.userId];
NSUInteger oneSelfPowerLevel = [powerLevels powerLevelOfUserWithUserID:mxHandler.userId];
[buttonsTitles removeAllObjects];
// Consider the case of the user himself
if ([_mxRoomMember.userId isEqualToString:mxHandler.userId]) {
[buttonsTitles addObject:@"Leave"];
if (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomPowerLevels]) {
[buttonsTitles addObject:@"Set power level"];
}
} else {
// Consider membership of the selected member
switch (_mxRoomMember.membership) {
case MXMembershipInvite:
case MXMembershipJoin: {
// Check conditions to be able to kick someone
if (oneSelfPowerLevel >= [powerLevels kick] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Kick"];
}
// Check conditions to be able to ban someone
if (oneSelfPowerLevel >= [powerLevels ban] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Ban"];
}
break;
}
case MXMembershipLeave: {
// Check conditions to be able to invite someone
if (oneSelfPowerLevel >= [powerLevels invite]) {
[buttonsTitles addObject:@"Invite"];
}
// Check conditions to be able to ban someone
if (oneSelfPowerLevel >= [powerLevels ban] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Ban"];
}
break;
}
case MXMembershipBan: {
// Check conditions to be able to unban someone
if (oneSelfPowerLevel >= [powerLevels ban] && oneSelfPowerLevel >= memberPowerLevel) {
[buttonsTitles addObject:@"Unban"];
}
break;
}
default: {
break;
}
}
// update power level
if (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomPowerLevels]) {
[buttonsTitles addObject:@"Set power level"];
}
// offer to start a new chat only if the room is not a 1:1 room with this user
// it does not make sense : it would open the same room
NSString* roomId = [mxHandler getRoomStartedWithMember:_mxRoomMember];
if (![roomId isEqualToString:mxRoom.state.roomId]) {
[buttonsTitles addObject:@"Start chat"];
}
}
return (buttonsTitles.count + 1) / 2;
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.tableView == aTableView) {
NSInteger row = indexPath.row;
MemberActionsCell* memberActionsCell = (MemberActionsCell*)[aTableView dequeueReusableCellWithIdentifier:@"MemberActionsCell" forIndexPath:indexPath];
NSString* leftTitle = nil;
NSString* rightTitle = nil;
if ((row * 2) < buttonsTitles.count) {
leftTitle = [buttonsTitles objectAtIndex:row * 2];
}
if (((row * 2) + 1) < buttonsTitles.count) {
rightTitle = [buttonsTitles objectAtIndex:(row * 2) + 1];
}
[memberActionsCell setLeftButtonText:leftTitle];
[memberActionsCell setRightButtonText:rightTitle];
return memberActionsCell;
}
return nil;
}
#pragma mark - button management
- (BOOL)hasPendingAction {
return nil != pendingMaskSpinnerView;
}
- (void) addPendingActionMask {
// add a spinner above the tableview to avoid that the user tap on any other button
pendingMaskSpinnerView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
pendingMaskSpinnerView.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.5];
pendingMaskSpinnerView.frame = self.tableView.frame;
pendingMaskSpinnerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
// append it
[self.tableView.superview addSubview:pendingMaskSpinnerView];
// animate it
[pendingMaskSpinnerView startAnimating];
}
- (void) removePendingActionMask {
if (pendingMaskSpinnerView) {
[pendingMaskSpinnerView removeFromSuperview];
pendingMaskSpinnerView = nil;
[self.tableView reloadData];
}
}
- (void) setUserPowerLevel:(MXRoomMember*)roomMember to:(int)value {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
int currentPowerLevel = (int)([mxHandler getPowerLevel:roomMember inRoom:self.mxRoom] * 100);
// check if the power level has not yet been set to 0
if (value != currentPowerLevel) {
__weak typeof(self) weakSelf = self;
[weakSelf addPendingActionMask];
// Reset user power level
[self.mxRoom setPowerLevelOfUserWithUserID:roomMember.userId powerLevel:value success:^{
[weakSelf removePendingActionMask];
} failure:^(NSError *error) {
[weakSelf removePendingActionMask];
NSLog(@"Set user power (%@) failed: %@", roomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}
- (void) updateUserPowerLevel:(MXRoomMember*)roomMember {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
__weak typeof(self) weakSelf = self;
// Ask for userId to invite
self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Power Level" message:nil style:CustomAlertStyleAlert];
if (![mxHandler.userId isEqualToString:roomMember.userId]) {
self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Reset to default" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
weakSelf.actionMenu = nil;
[weakSelf setUserPowerLevel:roomMember to:0];
}];
}
[self.actionMenu addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
textField.text = [NSString stringWithFormat:@"%d", (int)([mxHandler getPowerLevel:roomMember inRoom:weakSelf.mxRoom] * 100)];
textField.placeholder = nil;
textField.keyboardType = UIKeyboardTypeDecimalPad;
}];
[self.actionMenu addActionWithTitle:@"OK" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
UITextField *textField = [alert textFieldAtIndex:0];
weakSelf.actionMenu = nil;
if (textField.text.length > 0) {
[weakSelf setUserPowerLevel:roomMember to:(int)[textField.text integerValue]];
}
}];
[self.actionMenu showInViewController:self];
}
- (IBAction)onButtonToggle:(id)sender {
if ([sender isKindOfClass:[UIButton class]]) {
// already a pending action
if ([self hasPendingAction]) {
return;
}
NSString* text = ((UIButton*)sender).titleLabel.text;
if ([text isEqualToString:@"Leave"]) {
[self addPendingActionMask];
[self.mxRoom leave:^{
[self removePendingActionMask];
[self.navigationController popToRootViewControllerAnimated:NO];
[[AppDelegate theDelegate].masterTabBarController setVisibleRoomId:nil];
[[AppDelegate theDelegate].masterTabBarController popRoomViewControllerAnimated:YES];
} failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"Leave room %@ failed: %@", mxRoom.state.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Set power level"]) {
[self updateUserPowerLevel:_mxRoomMember];
} else if ([text isEqualToString:@"Kick"]) {
[self addPendingActionMask];
[mxRoom kickUser:_mxRoomMember.userId
reason:nil
success:^{
[self removePendingActionMask];
[self.navigationController popToRootViewControllerAnimated:YES];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"Kick %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Ban"]) {
[self addPendingActionMask];
[mxRoom banUser:_mxRoomMember.userId
reason:nil
success:^{
[self removePendingActionMask];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"Ban %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Invite"]) {
[self addPendingActionMask];
[mxRoom inviteUser:_mxRoomMember.userId
success:^{
[self removePendingActionMask];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"Invite %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Unban"]) {
[self addPendingActionMask];
[mxRoom unbanUser:_mxRoomMember.userId
success:^{
[self removePendingActionMask];
}
failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"Unban %@ failed: %@", _mxRoomMember.userId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
} else if ([text isEqualToString:@"Start chat"]) {
[self addPendingActionMask];
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
NSString* roomId = [mxHandler getRoomStartedWithMember:_mxRoomMember];
// if the room has already been started
if (roomId) {
// open it
[[AppDelegate theDelegate].masterTabBarController showRoom:roomId];
}
else {
// else create new room
[mxHandler.mxRestClient createRoom:nil
visibility:kMXRoomVisibilityPrivate
roomAlias:nil
topic:nil
success:^(MXCreateRoomResponse *response) {
[self removePendingActionMask];
// add the user
[mxHandler.mxRestClient inviteUser:_mxRoomMember.userId toRoom:response.roomId success:^{
//NSLog(@"%@ has been invited (roomId: %@)", roomMember.userId, response.roomId);
} failure:^(NSError *error) {
NSLog(@"%@ invitation failed (roomId: %@): %@", _mxRoomMember.userId, response.roomId, error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
// Open created room
[[AppDelegate theDelegate].masterTabBarController showRoom:response.roomId];
} failure:^(NSError *error) {
[self removePendingActionMask];
NSLog(@"Create room failed: %@", error);
//Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
}
}
}
@end

View file

@ -27,6 +27,7 @@
// Array of RecentRooms
NSMutableArray *recents;
id recentsListener;
NSUInteger unreadCount;
// Search
UISearchBar *recentsSearchBar;
@ -37,6 +38,7 @@
NSDateFormatter *dateFormatter;
RoomViewController *currentRoomViewController;
BOOL isVisible;
}
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@ -72,6 +74,7 @@
// Initialisation
recents = nil;
filteredRecents = nil;
unreadCount = 0;
NSString *dateFormat = @"MMM dd HH:mm";
dateFormatter = [[NSDateFormatter alloc] init];
@ -80,7 +83,7 @@
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
[dateFormatter setDateFormat:dateFormat];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isInitialSyncDone" options:0 context:nil];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"status" options:0 context:nil];
}
- (void)dealloc {
@ -100,7 +103,7 @@
if (dateFormatter) {
dateFormatter = nil;
}
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isInitialSyncDone"];
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"status"];
}
- (void)didReceiveMemoryWarning {
@ -135,6 +138,8 @@
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
isVisible = YES;
// Release potential Room ViewController if none is visible (Note: check on room visibility is required to handle correctly splitViewController)
if ([AppDelegate theDelegate].masterTabBarController.visibleRoomId == nil && currentRoomViewController) {
currentRoomViewController.roomId = nil;
@ -142,6 +147,12 @@
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
isVisible = NO;
}
#pragma mark -
- (void)setPreSelectedRoomId:(NSString *)roomId {
@ -182,15 +193,45 @@
#pragma mark - Internal methods
// remove the focus on a deleted room
// when the view is splitted between the recents and the selected rooms
- (void)checkSelectedRoomExists {
// IOS 8 only
if ([self.splitViewController respondsToSelector:@selector(isCollapsed)]) {
// there is a split view recents / chat view
if (!self.splitViewController.isCollapsed && currentRoomViewController.roomId) {
// check if the room still exists
BOOL exists = NO;
for(RecentRoom* recentRoom in recents) {
exists |= [recentRoom.roomId isEqualToString:currentRoomViewController.roomId];
}
// if it does not exist anymore
if (!exists) {
// release the room viewController
currentRoomViewController.roomId = nil;
currentRoomViewController = nil;
// delete the selected row
[self.tableView selectRowAtIndexPath:nil animated:NO scrollPosition: UITableViewScrollPositionNone];
}
}
}
}
- (void)configureView {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[self startActivityIndicator];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kRecentRoomUpdatedByBackPagination object:nil];
if ([mxHandler isInitialSyncDone]) {
// Create/Update recents
if (mxHandler.mxSession) {
if (mxHandler.mxSession) {
// Check matrix handler status
if (mxHandler.status == MatrixHandlerStatusStoreDataReady) {
// Server sync is not complete yet
if (!recents) {
// Retrieve recents from local storage (some data may not be up-to-date)
NSArray *recentEvents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
recents = [NSMutableArray arrayWithCapacity:recentEvents.count];
for (MXEvent *mxEvent in recentEvents) {
@ -200,6 +241,21 @@
[recents addObject:recentRoom];
}
}
unreadCount = 0;
}
} else if (mxHandler.status == MatrixHandlerStatusServerSyncDone) {
// Force recents refresh and add listener to update them (if it is not already done)
if (!recentsListener) {
NSArray *recentEvents = [NSMutableArray arrayWithArray:[mxHandler.mxSession recentsWithTypeIn:mxHandler.eventsFilterForMessages]];
recents = [NSMutableArray arrayWithCapacity:recentEvents.count];
for (MXEvent *mxEvent in recentEvents) {
MXRoom *mxRoom = [mxHandler.mxSession roomWithRoomId:mxEvent.roomId];
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:mxEvent andRoomState:mxRoom.state markAsUnread:NO];
if (recentRoom) {
[recents addObject:recentRoom];
}
}
unreadCount = 0;
// Register recent listener
recentsListener = [mxHandler.mxSession listenToEventsOfTypes:mxHandler.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) {
@ -227,6 +283,10 @@
// Move this room at first position
[recents removeObjectAtIndex:index];
[recents insertObject:recentRoom atIndex:0];
if (isUnread) {
unreadCount++;
[self updateTitleView];
}
}
}
break;
@ -237,9 +297,15 @@
RecentRoom *recentRoom = [[RecentRoom alloc] initWithLastEvent:event andRoomState:roomState markAsUnread:isUnread];
if (recentRoom) {
[recents insertObject:recentRoom atIndex:0];
if (isUnread) {
unreadCount++;
[self updateTitleView];
}
}
}
[self checkSelectedRoomExists];
// Reload table
[self.tableView reloadData];
}
@ -265,13 +331,31 @@
[self.tableView reloadData];
}
if (!recents) {
if (recents) {
// Add observer to force refresh when a recent last description is updated thanks to back pagination
// (This happens when the current last event description is blank, a back pagination is triggered to display non empty description)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onRecentRoomUpdatedByBackPagination) name:kRecentRoomUpdatedByBackPagination object:nil];
} else {
// Remove potential listener
if (recentsListener && mxHandler.mxSession) {
[mxHandler.mxSession removeListener:recentsListener];
recentsListener = nil;
}
}
[self updateTitleView];
}
- (void)onRecentRoomUpdatedByBackPagination {
[self.tableView reloadData];
}
- (void)updateTitleView {
NSString *title = @"Recents";
if (unreadCount) {
title = [NSString stringWithFormat:@"Recents (%tu)", unreadCount];
}
self.navigationItem.title = title;
}
- (void)createNewRoom:(id)sender {
@ -312,11 +396,11 @@
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"isInitialSyncDone" isEqualToString:keyPath]) {
if ([@"status" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
// Hide the activity indicator when Recents is not the current tab
if ([AppDelegate theDelegate].masterTabBarController.selectedIndex != TABBAR_RECENTS_INDEX) {
// Hide the activity indicator when Recents is not visible
if (!isVisible) {
[self stopActivityIndicator];
}
});
@ -359,7 +443,9 @@
}
// Reset unread count for this room
unreadCount -= recentRoom.unreadCount;
[recentRoom resetUnreadCount];
[self updateTitleView];
if (self.splitViewController) {
// Refresh display (required in case of splitViewController)
@ -440,7 +526,7 @@
// set background color
if (recentRoom.unreadCount) {
cell.backgroundColor = [UIColor colorWithRed:1 green:0.9 blue:0.9 alpha:1.0];
cell.roomTitle.text = [NSString stringWithFormat:@"%@ (%lu)", cell.roomTitle.text, (unsigned long)recentRoom.unreadCount];
cell.roomTitle.text = [NSString stringWithFormat:@"%@ (%tu)", cell.roomTitle.text, recentRoom.unreadCount];
} else {
cell.backgroundColor = [UIColor clearColor];
}
@ -454,6 +540,7 @@
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Leave the selected room
RecentRoom *recentRoom;
if (filteredRecents) {
@ -470,6 +557,8 @@
[recents removeObjectAtIndex:indexPath.row];
}
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self checkSelectedRoomExists];
} failure:^(NSError *error) {
NSLog(@"Failed to leave room (%@) failed: %@", recentRoom.roomId, error);
//Alert user

File diff suppressed because it is too large Load diff

View file

@ -20,5 +20,8 @@
- (void)reset;
typedef void (^blockSettings_onCheckSave)();
- (BOOL)checkPendingSave:(blockSettings_onCheckSave)handler;
@end

View file

@ -29,11 +29,18 @@
#define SETTINGS_SECTION_CONFIGURATION_INDEX 2
#define SETTINGS_SECTION_COMMANDS_INDEX 3
#define SETTINGS_SECTION_ROOMS_DISPLAY_ALL_EVENTS_INDEX 0
#define SETTINGS_SECTION_ROOMS_HIDE_UNSUPPORTED_MESSAGES_INDEX 1
#define SETTINGS_SECTION_ROOMS_SORT_MEMBERS_INDEX 2
#define SETTINGS_SECTION_ROOMS_DISPLAY_LEFT_MEMBERS_INDEX 3
#define SETTINGS_SECTION_ROOMS_CLEAR_CACHE_INDEX 4
#define SETTINGS_SECTION_ROOMS_INDEX_COUNT 5
NSString* const kConfigurationFormatText = @"Home server: %@\r\nIdentity server: %@\r\nUser ID: %@\r\nAccess token: %@";
NSString* const kCommandsDescriptionText = @"The following commands are available in the room chat:\r\n\r\n /nick <display_name>: change your display name\r\n /me <action>: send the action you are doing. /me will be replaced by your display name\r\n /join <room_alias>: join a room\r\n /kick <user_id> [<reason>]: kick the user\r\n /ban <user_id> [<reason>]: ban the user\r\n /unban <user_id>: unban the user\r\n /op <user_id> <power_level>: set user power level\r\n /deop <user_id>: reset user power level to the room default value";
@interface SettingsViewController () {
id imageLoader;
MediaLoader *imageLoader;
NSString *currentDisplayName;
NSString *currentPictureURL;
@ -51,13 +58,33 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
UISwitch *unsupportedMsgSwitch;
UISwitch *sortMembersSwitch;
UISwitch *displayLeftMembersSwitch;
// user info update
BOOL isAvatarUpdated;
BOOL isDisplayNameUpdated;
// do not hide the spinner while switching between viewcontroller
BOOL isAvatarUploading;
BOOL isDisplayNameUploading;
//
UITextField* wordsListTextField;
// dynamic rows in the notification settings
int enableInAppRowIndex;
int setInAppWordRowIndex;
int enablePushNotificationdRowIndex;
}
@property (strong, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UIView *tableHeader;
@property (weak, nonatomic) IBOutlet UIButton *userPicture;
@property (weak, nonatomic) IBOutlet UITextField *userDisplayName;
@property (weak, nonatomic) IBOutlet UIButton *saveUserInfoButton;
@property (strong, nonatomic) IBOutlet UIView *activityIndicatorBackgroundView;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@property (strong, nonatomic) CustomAlert* customAlert;
- (IBAction)onButtonPressed:(id)sender;
@end
@ -82,7 +109,16 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
[[self.userPicture imageView] setClipsToBounds:YES];
errorAlerts = [NSMutableArray array];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isInitialSyncDone" options:0 context:nil];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"status" options:0 context:nil];
isAvatarUpdated = NO;
isDisplayNameUpdated = NO;
isAvatarUploading = NO;
isDisplayNameUploading = NO;
_saveUserInfoButton.enabled = NO;
_activityIndicatorBackgroundView.hidden = YES;
}
- (void)didReceiveMemoryWarning {
@ -90,7 +126,7 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
// Dispose of any resources that can be recreated.
if (imageLoader) {
[MediaManager cancel:imageLoader];
[imageLoader cancel];
imageLoader = nil;
}
}
@ -107,13 +143,14 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
unsupportedMsgSwitch = nil;
sortMembersSwitch = nil;
displayLeftMembersSwitch = nil;
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isInitialSyncDone"];
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"status"];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Refresh display
[self startUserInfoUploadAnimation];
[self configureView];
[[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isResumeDone" options:0 context:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAPNSHandlerHasBeenUpdated) name:kAPNSHandlerHasBeenUpdated object:nil];
@ -121,10 +158,38 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isResumeDone"];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kAPNSHandlerHasBeenUpdated object:nil];
}
- (BOOL)checkPendingSave:(blockSettings_onCheckSave)handler {
// there is a profile update and there is no pending update
if ((isAvatarUpdated || isDisplayNameUpdated) && (!isDisplayNameUploading) && (!isAvatarUploading)) {
dispatch_async(dispatch_get_main_queue(), ^{
__weak typeof(self) weakSelf = self;
self.customAlert = [[CustomAlert alloc] initWithTitle:nil message:@"Save profile update" style:CustomAlertStyleAlert];
self.customAlert.cancelButtonIndex = [self.customAlert addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
handler();
weakSelf.customAlert = nil;
}];
[self.customAlert addActionWithTitle:@"OK" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) {
[weakSelf saveDisplayName];
weakSelf.customAlert = nil;
handler();
}];
[self.customAlert showInViewController:self];
});
return YES;
}
return NO;
}
#pragma mark - Internal methods
- (void)onAPNSHandlerHasBeenUpdated {
@ -133,10 +198,18 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
[self.tableView reloadData];
}
- (void)updateAvatarImage:(UIImage*)image {
[self.userPicture setImage:image forState:UIControlStateNormal];
[self.userPicture setImage:image forState:UIControlStateHighlighted];
[self.userPicture setImage:image forState:UIControlStateDisabled];
}
- (void)reset {
// Remove observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
// Cancel picture loader (if any)
if (imageLoader) {
[MediaManager cancel:imageLoader];
[imageLoader cancel];
imageLoader = nil;
}
@ -153,29 +226,50 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
currentPictureURL = nil;
uploadedPictureURL = nil;
UIImage *image = [UIImage imageNamed:@"default-profile"];
[self.userPicture setImage:image forState:UIControlStateNormal];
[self.userPicture setImage:image forState:UIControlStateHighlighted];
[self updateAvatarImage:[UIImage imageNamed:@"default-profile"]];
currentDisplayName = nil;
self.userDisplayName.text = nil;
}
- (void) startUserInfoUploadAnimation {
if (_activityIndicatorBackgroundView.hidden) {
_activityIndicatorBackgroundView.hidden = NO;
[_activityIndicator startAnimating];
}
_saveUserInfoButton.enabled = NO;
}
- (void) stopUserInfoUploadAnimation {
if (!_activityIndicatorBackgroundView.hidden) {
_activityIndicatorBackgroundView.hidden = YES;
[_activityIndicator stopAnimating];
}
_saveUserInfoButton.enabled = isAvatarUpdated || isDisplayNameUpdated;
}
- (void)configureView {
// ignore any refresh until there is a pending upload
if (isDisplayNameUploading || isAvatarUploading) {
return;
}
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[_activityIndicator startAnimating];
// Disable user's interactions
_userPicture.enabled = NO;
_userDisplayName.enabled = NO;
if ([mxHandler isInitialSyncDone]) {
if (mxHandler.status == MatrixHandlerStatusServerSyncDone) {
if (!userUpdateListener) {
// Set current user's information and add observers
[self updateUserPicture:mxHandler.mxSession.myUser.avatarUrl];
currentDisplayName = mxHandler.mxSession.myUser.displayname;
self.userDisplayName.text = currentDisplayName;
[self stopUserInfoUploadAnimation];
// Register listener to update user's information
userUpdateListener = [mxHandler.mxSession.myUser listenToUserUpdate:^(MXEvent *event) {
// Update displayName
@ -185,15 +279,25 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
}
// Update user's avatar
[self updateUserPicture:mxHandler.mxSession.myUser.avatarUrl];
// update button management
isDisplayNameUpdated = isAvatarUpdated = NO;
_saveUserInfoButton.enabled = NO;
// TODO display user's presence
}];
}
} else {
} else if (mxHandler.status == MatrixHandlerStatusStoreDataReady) {
// Set local user's information (the data may not be up-to-date)
[self updateUserPicture:mxHandler.mxSession.myUser.avatarUrl];
currentDisplayName = mxHandler.mxSession.myUser.displayname;
self.userDisplayName.text = currentDisplayName;
} else if (mxHandler.status == MatrixHandlerStatusLoggedOut) {
[self reset];
}
if ([mxHandler isResumeDone]) {
[_activityIndicator stopAnimating];
[self stopUserInfoUploadAnimation];
_userPicture.enabled = YES;
_userDisplayName.enabled = YES;
}
@ -205,18 +309,31 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
NSString *displayname = self.userDisplayName.text;
if ((displayname.length || currentDisplayName.length) && [displayname isEqualToString:currentDisplayName] == NO) {
// Save display name
[_activityIndicator startAnimating];
[self startUserInfoUploadAnimation];
_userDisplayName.enabled = NO;
isDisplayNameUploading = YES;
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
[mxHandler.mxSession.myUser setDisplayName:displayname success:^{
// save the current displayname
currentDisplayName = displayname;
[_activityIndicator stopAnimating];
// no more update in progress
isDisplayNameUpdated = NO;
// need to uploaded the avatar
if (isAvatarUpdated) {
[self savePicture];
} else {
// the job is ended
[self stopUserInfoUploadAnimation];
}
_userDisplayName.enabled = YES;
isDisplayNameUploading = NO;
} failure:^(NSError *error) {
NSLog(@"Set displayName failed: %@", error);
[_activityIndicator stopAnimating];
[self stopUserInfoUploadAnimation];
_userDisplayName.enabled = YES;
isDisplayNameUploading = NO;
//Alert user
NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
@ -245,35 +362,54 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
// Save picture
[_activityIndicator startAnimating];
[self startUserInfoUploadAnimation];
_userPicture.enabled = NO;
isAvatarUploading = YES;
if (uploadedPictureURL == nil) {
// Upload picture
[mxHandler.mxRestClient uploadContent:UIImageJPEGRepresentation([self.userPicture imageForState:UIControlStateNormal], 0.5)
mimeType:@"image/jpeg"
timeout:30
success:^(NSString *url) {
// Store uploaded picture url and trigger picture saving
uploadedPictureURL = url;
[self savePicture];
} failure:^(NSError *error) {
NSLog(@"Upload image failed: %@", error);
[_activityIndicator stopAnimating];
_userPicture.enabled = YES;
[self handleErrorDuringPictureSaving:error];
}];
MediaLoader *uploader = [[MediaLoader alloc] initWithUploadId:nil initialRange:0 andRange:1.0];
[uploader uploadData:UIImageJPEGRepresentation([self.userPicture imageForState:UIControlStateNormal], 0.5) mimeType:@"image/jpeg" success:^(NSString *url) {
// Store uploaded picture url and trigger picture saving
uploadedPictureURL = url;
[self savePicture];
} failure:^(NSError *error) {
NSLog(@"Upload image failed: %@", error);
[self stopUserInfoUploadAnimation];
_userPicture.enabled = YES;
isAvatarUploading = NO;
[self handleErrorDuringPictureSaving:error];
}];
} else {
[mxHandler.mxSession.myUser setAvatarUrl:uploadedPictureURL
success:^{
// uploadedPictureURL becomes the uploaded picture
currentPictureURL = uploadedPictureURL;
// manage the nil case.
[self updateUserPicture:uploadedPictureURL];
uploadedPictureURL = nil;
[_activityIndicator stopAnimating];
isAvatarUpdated = NO;
if (isDisplayNameUpdated) {
[self saveDisplayName];
} else {
_saveUserInfoButton.enabled = NO;
[self stopUserInfoUploadAnimation];
}
// update statuses
_userPicture.enabled = YES;
isAvatarUploading = NO;
} failure:^(NSError *error) {
NSLog(@"Set avatar url failed: %@", error);
[_activityIndicator stopAnimating];
[self stopUserInfoUploadAnimation];
_userPicture.enabled = YES;
isAvatarUploading = NO;
// update statuses
[self handleErrorDuringPictureSaving:error];
}];
}
@ -304,27 +440,66 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
- (void)updateUserPicture:(NSString *)avatar_url {
if (currentPictureURL == nil || [currentPictureURL isEqualToString:avatar_url] == NO) {
// Remove any pending observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
// Cancel previous loader (if any)
if (imageLoader) {
[MediaManager cancel:imageLoader];
[imageLoader cancel];
imageLoader = nil;
}
currentPictureURL = [avatar_url isEqual:[NSNull null]] ? nil : avatar_url;
if (currentPictureURL) {
// Load user's picture
imageLoader = [MediaManager loadPicture:currentPictureURL success:^(UIImage *image) {
[self.userPicture setImage:image forState:UIControlStateNormal];
[self.userPicture setImage:image forState:UIControlStateHighlighted];
} failure:^(NSError *error) {
// Reset picture URL in order to try next time
currentPictureURL = nil;
}];
// Check whether the image download is in progress
id loader = [MediaManager existingDownloaderForURL:currentPictureURL];
if (loader) {
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
} else {
// Retrieve the image from cache
UIImage* image = [MediaManager loadCachePictureForURL:currentPictureURL];
if (image) {
[self updateAvatarImage:image];
} else {
// Cancel potential download in progress
if (imageLoader) {
[imageLoader cancel];
}
// Add observers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFinishNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMediaDownloadDidFailNotification object:nil];
imageLoader = [MediaManager downloadMediaFromURL:currentPictureURL withType:@"image/jpeg"];
}
}
} else {
// Set placeholder
UIImage *image = [UIImage imageNamed:@"default-profile"];
[self.userPicture setImage:image forState:UIControlStateNormal];
[self.userPicture setImage:image forState:UIControlStateHighlighted];
[self updateAvatarImage:[UIImage imageNamed:@"default-profile"]];
}
}
}
- (void)onMediaDownloadEnd:(NSNotification *)notif {
// sanity check
if ([notif.object isKindOfClass:[NSString class]]) {
NSString* url = notif.object;
if ([url isEqualToString:currentPictureURL]) {
// update the image
UIImage* image = [MediaManager loadCachePictureForURL:currentPictureURL];
if (image == nil) {
image = [UIImage imageNamed:@"default-profile"];
}
[self updateAvatarImage:image];
// remove the observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
imageLoader = nil;
if ([notif.name isEqualToString:kMediaDownloadDidFailNotification]) {
// Reset picture URL in order to try next time
currentPictureURL = nil;
}
}
}
}
@ -332,17 +507,17 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"isInitialSyncDone" isEqualToString:keyPath]) {
if ([@"status" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self configureView];
});
} else if ([@"isResumeDone" isEqualToString:keyPath]) {
if ([[MatrixHandler sharedHandler] isResumeDone]) {
[_activityIndicator stopAnimating];
[self stopUserInfoUploadAnimation];
_userPicture.enabled = YES;
_userDisplayName.enabled = YES;
} else {
[_activityIndicator startAnimating];
[self startUserInfoUploadAnimation];
_userPicture.enabled = NO;
_userDisplayName.enabled = NO;
}
@ -354,7 +529,15 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
- (IBAction)onButtonPressed:(id)sender {
[self dismissKeyboard];
if (sender == _userPicture) {
if (sender == _saveUserInfoButton) {
if (isDisplayNameUpdated) {
_saveUserInfoButton.enabled = NO;
[self saveDisplayName];
} else if (isAvatarUpdated) {
_saveUserInfoButton.enabled = NO;
[self savePicture];
}
} else if (sender == _userPicture) {
// Open picture gallery
UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init];
mediaPicker.delegate = self;
@ -367,6 +550,7 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
[APNSHandler sharedHandler].isActive = apnsNotificationsSwitch.on;
} else if (sender == inAppNotificationsSwitch) {
[AppSettings sharedSettings].enableInAppNotifications = inAppNotificationsSwitch.on;
[self.tableView reloadData];
} else if (sender == allEventsSwitch) {
[AppSettings sharedSettings].displayAllEvents = allEventsSwitch.on;
} else if (sender == unsupportedMsgSwitch) {
@ -380,21 +564,104 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
#pragma mark - keyboard
- (void) manageSaveChangeButton {
// check if there is a displayname update
NSString *displayname = self.userDisplayName.text;
isDisplayNameUpdated = ((displayname.length || currentDisplayName.length) && [displayname isEqualToString:currentDisplayName] == NO);
_saveUserInfoButton.enabled = isDisplayNameUpdated || isAvatarUpdated;
}
// remove trailing spaces
- (NSString*)removeUselessSpaceChars:(NSString*)text {
NSMutableString* cleanedText = [text mutableCopy];
while ([cleanedText hasPrefix:@" "]) {
cleanedText = [[cleanedText substringFromIndex:1] mutableCopy];
}
while ([cleanedText hasSuffix:@" "]) {
cleanedText = [[cleanedText substringToIndex:cleanedText.length-1] mutableCopy];
}
return cleanedText;
}
// split the words list provided by the user
// check if they are valid, not duplicated
- (void)manageWordsList {
NSArray* words = [wordsListTextField.text componentsSeparatedByString:@","];
NSMutableArray* fiteredWords = [[NSMutableArray alloc] init];
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
// theses both items are implicitly checked
NSString* displayname = nil;
if (mxHandler.mxSession.myUser.displayname.length) {
displayname = mxHandler.mxSession.myUser.displayname;
}
NSString* userID = nil;
if (mxHandler.localPartFromUserId.length) {
userID = mxHandler.localPartFromUserId;
}
// checked word by word
for(NSString* word in words) {
NSString* cleanWord = [self removeUselessSpaceChars:word];
// if they are valid (not null, not implicit and does not already added
if ((cleanWord.length > 0) && ![cleanWord isEqualToString:displayname] && ![cleanWord isEqualToString:userID] && ([fiteredWords indexOfObject:cleanWord] == NSNotFound)) {
[fiteredWords addObject:cleanWord];
}
}
[[AppSettings sharedSettings] setSpecificWordsToAlertOn:fiteredWords];
[self refreshWordsList];
}
- (void)dismissKeyboard {
// Hide the keyboard
[_userDisplayName resignFirstResponder];
// Save display name change (if any)
[self saveDisplayName];
if ([_userDisplayName isFirstResponder]) {
// Hide the keyboard
[_userDisplayName resignFirstResponder];
[self manageSaveChangeButton];
}
if ([wordsListTextField isFirstResponder]) {
[self manageWordsList];
[wordsListTextField resignFirstResponder];
}
}
#pragma mark - UITextField delegate
- (BOOL)textFieldShouldReturn:(UITextField*) textField {
// "Done" key has been pressed
[self dismissKeyboard];
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
if ((_userDisplayName == textField) || (wordsListTextField == textField)) {
// "Done" key has been pressed
[self dismissKeyboard];
}
return YES;
}
- (IBAction)textFieldDidChange:(id)sender {
if (sender == _userDisplayName) {
[self manageSaveChangeButton];
}
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.tableView == aTableView) {
// tap on clear application cache
if ((indexPath.section == SETTINGS_SECTION_ROOMS_INDEX) && (indexPath.row == SETTINGS_SECTION_ROOMS_CLEAR_CACHE_INDEX)) {
// clear caches
[[MatrixHandler sharedHandler] forceInitialSync:YES];
}
[aTableView deselectRowAtIndexPath:indexPath animated:YES];
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
@ -403,12 +670,23 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) {
if ([APNSHandler sharedHandler].isAvailable) {
return 2;
enableInAppRowIndex = setInAppWordRowIndex = enablePushNotificationdRowIndex = -1;
int count = 0;
enableInAppRowIndex = count++;
if ([[AppSettings sharedSettings] enableInAppNotifications]) {
setInAppWordRowIndex = count++;
}
return 1;
if ([APNSHandler sharedHandler].isAvailable) {
enablePushNotificationdRowIndex = count++;
}
return count;
} else if (section == SETTINGS_SECTION_ROOMS_INDEX) {
return 4;
return SETTINGS_SECTION_ROOMS_INDEX_COUNT;
} else if (section == SETTINGS_SECTION_CONFIGURATION_INDEX) {
return 1;
} else if (section == SETTINGS_SECTION_COMMANDS_INDEX) {
@ -420,6 +698,10 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) {
if (indexPath.row == setInAppWordRowIndex) {
return 110;
}
return 44;
} else if (indexPath.section == SETTINGS_SECTION_ROOMS_INDEX) {
return 44;
@ -467,41 +749,88 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
return sectionHeader;
}
- (void)refreshWordsList {
NSMutableString* wordsList = [[NSMutableString alloc] init];
NSArray* patterns = [AppSettings sharedSettings].specificWordsToAlertOn;
for(NSString* string in patterns) {
[wordsList appendFormat:@"%@,", string];
}
if (wordsList.length > 0) {
wordsListTextField.text = [wordsList substringToIndex:wordsList.length - 1];
}
else {
wordsListTextField.text = nil;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
SettingsTableViewCell *cell = nil;
UITableViewCell *cell = nil;
if (indexPath.section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) {
SettingsTableCellWithSwitch *notificationsCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithSwitch" forIndexPath:indexPath];
if (indexPath.row == 0) {
notificationsCell.settingLabel.text = @"Enable In-App notifications";
notificationsCell.settingSwitch.on = [[AppSettings sharedSettings] enableInAppNotifications];
inAppNotificationsSwitch = notificationsCell.settingSwitch;
} else {
notificationsCell.settingLabel.text = @"Enable push notifications";
notificationsCell.settingSwitch.on = [[APNSHandler sharedHandler] isActive];
apnsNotificationsSwitch = notificationsCell.settingSwitch;
if (indexPath.row == setInAppWordRowIndex) {
SettingsCellWithLabelAndTextField* settingsCellWithLabelAndTextField = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithLabelAndTextField" forIndexPath:indexPath];
settingsCellWithLabelAndTextField.settingTextField.delegate = self;
wordsListTextField = settingsCellWithLabelAndTextField.settingTextField;
// update the text only if it is not the first responder
if (!wordsListTextField.isFirstResponder) {
[self refreshWordsList];
}
cell = settingsCellWithLabelAndTextField;
}
else {
SettingsTableCellWithSwitch *notificationsCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithSwitch" forIndexPath:indexPath];
if (indexPath.row == enableInAppRowIndex) {
notificationsCell.settingLabel.text = @"Enable In-App notifications";
notificationsCell.settingSwitch.on = [[AppSettings sharedSettings] enableInAppNotifications];
inAppNotificationsSwitch = notificationsCell.settingSwitch;
} else /* SETTINGS_SECTION_NOTIFICATIONS_PUSH_NOTIFICATION_INDEX */{
notificationsCell.settingLabel.text = @"Enable push notifications";
notificationsCell.settingSwitch.on = [[APNSHandler sharedHandler] isActive];
apnsNotificationsSwitch = notificationsCell.settingSwitch;
}
cell = notificationsCell;
}
cell = notificationsCell;
} else if (indexPath.section == SETTINGS_SECTION_ROOMS_INDEX) {
SettingsTableCellWithSwitch *roomsSettingCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithSwitch" forIndexPath:indexPath];
if (indexPath.row == 0) {
roomsSettingCell.settingLabel.text = @"Display all events";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] displayAllEvents];
allEventsSwitch = roomsSettingCell.settingSwitch;
} else if (indexPath.row == 1) {
roomsSettingCell.settingLabel.text = @"Hide unsupported messages";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] hideUnsupportedMessages];
unsupportedMsgSwitch = roomsSettingCell.settingSwitch;
} else if (indexPath.row == 2) {
roomsSettingCell.settingLabel.text = @"Sort members by last seen time";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] sortMembersUsingLastSeenTime];
sortMembersSwitch = roomsSettingCell.settingSwitch;
if (indexPath.row == SETTINGS_SECTION_ROOMS_CLEAR_CACHE_INDEX) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ClearCacheCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ClearCacheCell"];
}
cell.textLabel.text = [NSString stringWithFormat:@"Clear cache (%@)", [NSByteCountFormatter stringFromByteCount:[MatrixHandler sharedHandler].cachesSize countStyle:NSByteCountFormatterCountStyleFile]];
;
cell.textLabel.textAlignment = NSTextAlignmentCenter;
cell.textLabel.textColor = [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor;
} else {
roomsSettingCell.settingLabel.text = @"Display left members";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] displayLeftUsers];
displayLeftMembersSwitch = roomsSettingCell.settingSwitch;
SettingsTableCellWithSwitch *roomsSettingCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithSwitch" forIndexPath:indexPath];
if (indexPath.row == SETTINGS_SECTION_ROOMS_DISPLAY_ALL_EVENTS_INDEX) {
roomsSettingCell.settingLabel.text = @"Display all events";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] displayAllEvents];
allEventsSwitch = roomsSettingCell.settingSwitch;
} else if (indexPath.row == SETTINGS_SECTION_ROOMS_HIDE_UNSUPPORTED_MESSAGES_INDEX) {
roomsSettingCell.settingLabel.text = @"Hide unsupported messages";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] hideUnsupportedMessages];
unsupportedMsgSwitch = roomsSettingCell.settingSwitch;
} else if (indexPath.row == SETTINGS_SECTION_ROOMS_SORT_MEMBERS_INDEX) {
roomsSettingCell.settingLabel.text = @"Sort members by last seen time";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] sortMembersUsingLastSeenTime];
sortMembersSwitch = roomsSettingCell.settingSwitch;
} else if (indexPath.row == SETTINGS_SECTION_ROOMS_DISPLAY_LEFT_MEMBERS_INDEX) {
roomsSettingCell.settingLabel.text = @"Display left members";
roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] displayLeftUsers];
displayLeftMembersSwitch = roomsSettingCell.settingSwitch;
}
cell = roomsSettingCell;
}
cell = roomsSettingCell;
} else if (indexPath.section == SETTINGS_SECTION_CONFIGURATION_INDEX) {
SettingsTableCellWithTextView *configCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath];
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
@ -521,9 +850,9 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];
if (selectedImage) {
[self.userPicture setImage:selectedImage forState:UIControlStateNormal];
[self.userPicture setImage:selectedImage forState:UIControlStateHighlighted];
[self savePicture];
[self updateAvatarImage:selectedImage];
isAvatarUpdated = YES;
_saveUserInfoButton.enabled = YES;
}
[self dismissMediaPicker];
}
@ -536,4 +865,6 @@ NSString* const kCommandsDescriptionText = @"The following commands are availabl
[[AppDelegate theDelegate].masterTabBarController dismissMediaPicker];
}
# pragma mark - UITextViewDelegate
@end