mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-29 07:42:40 +00:00
Merge branch 'develop' into apns
This commit is contained in:
commit
53e80c5706
51 changed files with 4658 additions and 1228 deletions
|
@ -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
|
||||
|
|
|
@ -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``.
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
27
matrixConsole/matrixConsole/API/ConsoleTools.h
Normal file
27
matrixConsole/matrixConsole/API/ConsoleTools.h
Normal 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
|
91
matrixConsole/matrixConsole/API/ConsoleTools.m
Normal file
91
matrixConsole/matrixConsole/API/ConsoleTools.m
Normal 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
|
91
matrixConsole/matrixConsole/API/MediaLoader.h
Normal file
91
matrixConsole/matrixConsole/API/MediaLoader.h
Normal 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
|
259
matrixConsole/matrixConsole/API/MediaLoader.m
Normal file
259
matrixConsole/matrixConsole/API/MediaLoader.m
Normal 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
|
53
matrixConsole/matrixConsole/API/MediaManager.h
Normal file
53
matrixConsole/matrixConsole/API/MediaManager.h
Normal 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
|
224
matrixConsole/matrixConsole/API/MediaManager.m
Normal file
224
matrixConsole/matrixConsole/API/MediaManager.m
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 & 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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
#import <MatrixSDK/MatrixSDK.h>
|
||||
|
||||
extern NSString *const kLocalEchoEventIdPrefix;
|
||||
extern NSString *const kFailedEventId;
|
||||
extern NSString *const kFailedEventIdPrefix;
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
RoomMessageComponentStyleDefault,
|
||||
|
|
|
@ -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;
|
||||
|
|
40
matrixConsole/matrixConsole/View/CopyableLabel.h
Normal file
40
matrixConsole/matrixConsole/View/CopyableLabel.h
Normal 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
|
||||
|
244
matrixConsole/matrixConsole/View/CopyableLabel.m
Normal file
244
matrixConsole/matrixConsole/View/CopyableLabel.m
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
27
matrixConsole/matrixConsole/View/MemberActionsCell.h
Normal file
27
matrixConsole/matrixConsole/View/MemberActionsCell.h
Normal 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
|
||||
|
42
matrixConsole/matrixConsole/View/MemberActionsCell.m
Normal file
42
matrixConsole/matrixConsole/View/MemberActionsCell.m
Normal 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
|
28
matrixConsole/matrixConsole/View/PieChartView.h
Normal file
28
matrixConsole/matrixConsole/View/PieChartView.h
Normal 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
|
||||
|
141
matrixConsole/matrixConsole/View/PieChartView.m
Normal file
141
matrixConsole/matrixConsole/View/PieChartView.m
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -23,4 +23,7 @@
|
|||
@end
|
||||
|
||||
@implementation SettingsTableCellWithTextView
|
||||
@end
|
||||
|
||||
@implementation SettingsCellWithLabelAndTextField
|
||||
@end
|
|
@ -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 {
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
- (void)popRoomViewControllerAnimated:(BOOL)animated;
|
||||
|
||||
- (BOOL)isPresentingMediaPicker;
|
||||
- (void)presentMediaPicker:(UIImagePickerController*)mediaPicker;
|
||||
- (void)dismissMediaPicker;
|
||||
|
||||
|
|
|
@ -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:^{
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
@ -20,5 +20,8 @@
|
|||
|
||||
- (void)reset;
|
||||
|
||||
typedef void (^blockSettings_onCheckSave)();
|
||||
- (BOOL)checkPendingSave:(blockSettings_onCheckSave)handler;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue