commit 6156cb0f78e1b0038d5279f5cc21cd4e55055543 Author: giomfo Date: Mon Nov 24 10:38:23 2014 +0100 Rename directories of sample sources (matrixConsole) diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e0217c405 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control +# +Pods/ + +# Do not track our workspace since it is created by CocoaPods +*.xcworkspace diff --git a/Podfile b/Podfile new file mode 100644 index 000000000..15bb881f1 --- /dev/null +++ b/Podfile @@ -0,0 +1,16 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, "6.0" + +source 'https://github.com/CocoaPods/Specs.git' + +target "matrixConsole" do + +# Points to SDK local sources file +pod 'MatrixSDK', :path => '../../MatrixSDK.podspec' + +end + +target "matrixConsole" do + +end + diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 000000000..672f5f741 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,42 @@ +PODS: + - AFNetworking (2.4.1): + - AFNetworking/NSURLConnection + - AFNetworking/NSURLSession + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/UIKit + - AFNetworking/NSURLConnection (2.4.1): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/NSURLSession (2.4.1): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/Reachability (2.4.1) + - AFNetworking/Security (2.4.1) + - AFNetworking/Serialization (2.4.1) + - AFNetworking/UIKit (2.4.1): + - AFNetworking/NSURLConnection + - AFNetworking/NSURLSession + - Mantle (1.5.1): + - Mantle/extobjc + - Mantle/extobjc (1.5.1) + - MatrixSDK (0.0.1): + - AFNetworking (~> 2.4.1) + - Mantle (~> 1.5) + +DEPENDENCIES: + - MatrixSDK (from `../../MatrixSDK.podspec`) + +EXTERNAL SOURCES: + MatrixSDK: + :path: ../../MatrixSDK.podspec + +SPEC CHECKSUMS: + AFNetworking: 0aabc6fae66d6e5d039eeb21c315843c7aae51ab + Mantle: d7c5ac734579ec751c58fecbf56189853056c58c + MatrixSDK: 42aac6ed0123d5339f27e3f57d8972edf17f8224 + +COCOAPODS: 0.34.1 diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..c3c3070f2 --- /dev/null +++ b/README.rst @@ -0,0 +1,11 @@ +Build instructions +================== + +Before opening the sample workspace, you need to build it with the CocoaPods command:: + + $ pod install + +This will load your local SDK source code into the sample workspace. + + +Then, open ``syMessaging.xcworkspace``. diff --git a/matrixConsole.xcodeproj/project.pbxproj b/matrixConsole.xcodeproj/project.pbxproj new file mode 100644 index 000000000..5ce5285dc --- /dev/null +++ b/matrixConsole.xcodeproj/project.pbxproj @@ -0,0 +1,640 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D648B86A591308736E2D4078 /* libPods-matrixConsole.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8141B1E2401FFCC3C5B99234 /* libPods-matrixConsole.a */; }; + F00B5DB91A1B9BCE00EA1C8D /* CustomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = F00B5DB81A1B9BCE00EA1C8D /* CustomImageView.m */; }; + F01628C119E29C660071C473 /* default-profile.png in Resources */ = {isa = PBXBuildFile; fileRef = F01628BC19E29C660071C473 /* default-profile.png */; }; + F01628C319E29C660071C473 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = F01628BE19E29C660071C473 /* logo.png */; }; + F024098219E7D177006E741B /* tab_recents@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F024098119E7D177006E741B /* tab_recents@2x.png */; }; + 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 */; }; + F03EF5F619F171EB00A0EE52 /* HomeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5EB19F171EB00A0EE52 /* HomeViewController.m */; }; + F03EF5F719F171EB00A0EE52 /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5ED19F171EB00A0EE52 /* LoginViewController.m */; }; + F03EF5F819F171EB00A0EE52 /* MasterTabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5EF19F171EB00A0EE52 /* MasterTabBarController.m */; }; + F03EF5F919F171EB00A0EE52 /* RecentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03EF5F119F171EB00A0EE52 /* RecentsViewController.m */; }; + 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 */; }; + F05B955F19DEED8A008761B0 /* MatrixHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = F05B955E19DEED8A008761B0 /* MatrixHandler.m */; }; + F07A80D819DD9DE700B621A1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F07A80D719DD9DE700B621A1 /* main.m */; }; + F07A80DB19DD9DE700B621A1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F07A80DA19DD9DE700B621A1 /* AppDelegate.m */; }; + F07A80E419DD9DE700B621A1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F07A80E219DD9DE700B621A1 /* Main.storyboard */; }; + F07A80E619DD9DE700B621A1 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F07A80E519DD9DE700B621A1 /* Images.xcassets */; }; + F07A80E919DD9DE700B621A1 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = F07A80E719DD9DE700B621A1 /* LaunchScreen.xib */; }; + F07A80F519DD9DE700B621A1 /* syMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F07A80F419DD9DE700B621A1 /* syMessagingTests.m */; }; + F08B6FCC1A1DE7F80094A35B /* matrixConsole.jpg in Resources */ = {isa = PBXBuildFile; fileRef = F08B6FCB1A1DE7F80094A35B /* matrixConsole.jpg */; }; + F08DCBDB1A093BFA008C65B6 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F08DCBDA1A093BFA008C65B6 /* MobileCoreServices.framework */; }; + F0CEA5AE19E6895E00E47915 /* logoHighRes.png in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5AC19E6895E00E47915 /* logoHighRes.png */; }; + F0CEA5AF19E6895E00E47915 /* tab_recents.png in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5AD19E6895E00E47915 /* tab_recents.png */; }; + F0CEA5B119E6898800E47915 /* tab_home.ico in Resources */ = {isa = PBXBuildFile; fileRef = F0CEA5B019E6898800E47915 /* tab_home.ico */; }; + F0D3C30C1A011EF10000D49E /* AppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = F0D3C30B1A011EF10000D49E /* AppSettings.m */; }; + F0D3C30F1A01330F0000D49E /* SettingsTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F0D3C30E1A01330F0000D49E /* SettingsTableViewCell.m */; }; + F0E84D401A1F9AEC005F2E42 /* RecentsTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E84D3F1A1F9AEC005F2E42 /* RecentsTableViewCell.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F07A80EF19DD9DE700B621A1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F07A80CA19DD9DE700B621A1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F07A80D119DD9DE700B621A1; + remoteInfo = syMessaging; + }; +/* End PBXContainerItemProxy section */ + +/* 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 = ""; }; + 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 = ""; }; + F00B5DB71A1B9BCE00EA1C8D /* CustomImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomImageView.h; sourceTree = ""; }; + F00B5DB81A1B9BCE00EA1C8D /* CustomImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomImageView.m; sourceTree = ""; }; + F01628BC19E29C660071C473 /* default-profile.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "default-profile.png"; sourceTree = ""; }; + F01628BE19E29C660071C473 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; + F024098119E7D177006E741B /* tab_recents@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_recents@2x.png"; sourceTree = ""; }; + F02BCE221A1A5A2B00543B47 /* play.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = play.png; sourceTree = ""; }; + F02D707419F1DC9E007B47D3 /* RoomMemberTableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMemberTableCell.h; sourceTree = ""; }; + F02D707519F1DC9E007B47D3 /* RoomMemberTableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMemberTableCell.m; sourceTree = ""; }; + F03C470F1A02952800E445AB /* CustomAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomAlert.h; sourceTree = ""; }; + F03C47101A02952800E445AB /* CustomAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomAlert.m; sourceTree = ""; }; + F03EF5EA19F171EB00A0EE52 /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = ""; }; + F03EF5EB19F171EB00A0EE52 /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = ""; }; + F03EF5EC19F171EB00A0EE52 /* LoginViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = ""; }; + F03EF5ED19F171EB00A0EE52 /* LoginViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = ""; }; + F03EF5EE19F171EB00A0EE52 /* MasterTabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MasterTabBarController.h; sourceTree = ""; }; + F03EF5EF19F171EB00A0EE52 /* MasterTabBarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MasterTabBarController.m; sourceTree = ""; }; + F03EF5F019F171EB00A0EE52 /* RecentsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RecentsViewController.h; sourceTree = ""; }; + F03EF5F119F171EB00A0EE52 /* RecentsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RecentsViewController.m; sourceTree = ""; }; + F03EF5F219F171EB00A0EE52 /* RoomViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomViewController.h; sourceTree = ""; }; + F03EF5F319F171EB00A0EE52 /* RoomViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomViewController.m; sourceTree = ""; }; + F03EF5F419F171EB00A0EE52 /* SettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = ""; }; + F03EF5F519F171EB00A0EE52 /* SettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsViewController.m; sourceTree = ""; }; + F03EF5FD19F1762000A0EE52 /* RoomMessageTableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMessageTableCell.h; sourceTree = ""; }; + F03EF5FE19F1762000A0EE52 /* RoomMessageTableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMessageTableCell.m; sourceTree = ""; }; + F03EF60019F19E7C00A0EE52 /* MediaManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaManager.h; sourceTree = ""; }; + F03EF60119F19E7C00A0EE52 /* MediaManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaManager.m; sourceTree = ""; }; + F05B955D19DEED8A008761B0 /* MatrixHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MatrixHandler.h; sourceTree = ""; }; + F05B955E19DEED8A008761B0 /* MatrixHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MatrixHandler.m; sourceTree = ""; }; + F07A80D219DD9DE700B621A1 /* matrixConsole.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = matrixConsole.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F07A80D619DD9DE700B621A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F07A80D719DD9DE700B621A1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + F07A80D919DD9DE700B621A1 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + F07A80DA19DD9DE700B621A1 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + F07A80E319DD9DE700B621A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + F07A80E519DD9DE700B621A1 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + F07A80E819DD9DE700B621A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + F07A80EE19DD9DE700B621A1 /* matrixConsoleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = matrixConsoleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F07A80F319DD9DE700B621A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F07A80F419DD9DE700B621A1 /* syMessagingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = syMessagingTests.m; sourceTree = ""; }; + F08B6FCB1A1DE7F80094A35B /* matrixConsole.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = matrixConsole.jpg; sourceTree = ""; }; + F08DCBDA1A093BFA008C65B6 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + F0CEA5AC19E6895E00E47915 /* logoHighRes.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logoHighRes.png; sourceTree = ""; }; + F0CEA5AD19E6895E00E47915 /* tab_recents.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_recents.png; sourceTree = ""; }; + F0CEA5B019E6898800E47915 /* tab_home.ico */ = {isa = PBXFileReference; lastKnownFileType = image.ico; path = tab_home.ico; sourceTree = ""; }; + F0D3C30A1A011EF10000D49E /* AppSettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppSettings.h; sourceTree = ""; }; + F0D3C30B1A011EF10000D49E /* AppSettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSettings.m; sourceTree = ""; }; + F0D3C30D1A01330F0000D49E /* SettingsTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsTableViewCell.h; sourceTree = ""; }; + F0D3C30E1A01330F0000D49E /* SettingsTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsTableViewCell.m; sourceTree = ""; }; + F0E84D3E1A1F9AEC005F2E42 /* RecentsTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RecentsTableViewCell.h; sourceTree = ""; }; + F0E84D3F1A1F9AEC005F2E42 /* RecentsTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RecentsTableViewCell.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F07A80CF19DD9DE700B621A1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F08DCBDB1A093BFA008C65B6 /* MobileCoreServices.framework in Frameworks */, + D648B86A591308736E2D4078 /* libPods-matrixConsole.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F07A80EB19DD9DE700B621A1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B18A10A9FB884DD357EBD414 /* Pods */ = { + isa = PBXGroup; + children = ( + B7EC7E45C718BF2BBCE0CF48 /* Pods-matrixConsole.debug.xcconfig */, + 13057A57E74FD5504196F47F /* Pods-matrixConsole.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + EAA71E3C2B16D1D86B3F5100 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F08DCBDA1A093BFA008C65B6 /* MobileCoreServices.framework */, + 8141B1E2401FFCC3C5B99234 /* libPods-matrixConsole.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F01628B519E298710071C473 /* Assets */ = { + isa = PBXGroup; + children = ( + F08B6FCB1A1DE7F80094A35B /* matrixConsole.jpg */, + F02BCE221A1A5A2B00543B47 /* play.png */, + F024098119E7D177006E741B /* tab_recents@2x.png */, + F0CEA5B019E6898800E47915 /* tab_home.ico */, + F0CEA5AD19E6895E00E47915 /* tab_recents.png */, + F01628BC19E29C660071C473 /* default-profile.png */, + F01628BE19E29C660071C473 /* logo.png */, + F0CEA5AC19E6895E00E47915 /* logoHighRes.png */, + ); + path = Assets; + sourceTree = ""; + }; + F03EF5E919F171EB00A0EE52 /* ViewController */ = { + isa = PBXGroup; + children = ( + F03EF5EA19F171EB00A0EE52 /* HomeViewController.h */, + F03EF5EB19F171EB00A0EE52 /* HomeViewController.m */, + F03EF5EC19F171EB00A0EE52 /* LoginViewController.h */, + F03EF5ED19F171EB00A0EE52 /* LoginViewController.m */, + F03EF5EE19F171EB00A0EE52 /* MasterTabBarController.h */, + F03EF5EF19F171EB00A0EE52 /* MasterTabBarController.m */, + F03EF5F019F171EB00A0EE52 /* RecentsViewController.h */, + F03EF5F119F171EB00A0EE52 /* RecentsViewController.m */, + F03EF5F219F171EB00A0EE52 /* RoomViewController.h */, + F03EF5F319F171EB00A0EE52 /* RoomViewController.m */, + F03EF5F419F171EB00A0EE52 /* SettingsViewController.h */, + F03EF5F519F171EB00A0EE52 /* SettingsViewController.m */, + ); + path = ViewController; + sourceTree = ""; + }; + F03EF5FC19F1762000A0EE52 /* View */ = { + isa = PBXGroup; + children = ( + F00B5DB71A1B9BCE00EA1C8D /* CustomImageView.h */, + F00B5DB81A1B9BCE00EA1C8D /* CustomImageView.m */, + F0E84D3E1A1F9AEC005F2E42 /* RecentsTableViewCell.h */, + F0E84D3F1A1F9AEC005F2E42 /* RecentsTableViewCell.m */, + F02D707419F1DC9E007B47D3 /* RoomMemberTableCell.h */, + F02D707519F1DC9E007B47D3 /* RoomMemberTableCell.m */, + F03EF5FD19F1762000A0EE52 /* RoomMessageTableCell.h */, + F03EF5FE19F1762000A0EE52 /* RoomMessageTableCell.m */, + F0D3C30D1A01330F0000D49E /* SettingsTableViewCell.h */, + F0D3C30E1A01330F0000D49E /* SettingsTableViewCell.m */, + ); + path = View; + sourceTree = ""; + }; + F07A80C919DD9DE700B621A1 = { + isa = PBXGroup; + children = ( + F07A80D419DD9DE700B621A1 /* syMessaging */, + F07A80F119DD9DE700B621A1 /* syMessagingTests */, + F07A80D319DD9DE700B621A1 /* Products */, + B18A10A9FB884DD357EBD414 /* Pods */, + EAA71E3C2B16D1D86B3F5100 /* Frameworks */, + ); + sourceTree = ""; + }; + F07A80D319DD9DE700B621A1 /* Products */ = { + isa = PBXGroup; + children = ( + F07A80D219DD9DE700B621A1 /* matrixConsole.app */, + F07A80EE19DD9DE700B621A1 /* matrixConsoleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F07A80D419DD9DE700B621A1 /* syMessaging */ = { + isa = PBXGroup; + children = ( + F03EF5FC19F1762000A0EE52 /* View */, + F03EF5E919F171EB00A0EE52 /* ViewController */, + F07A80D919DD9DE700B621A1 /* AppDelegate.h */, + F07A80DA19DD9DE700B621A1 /* AppDelegate.m */, + F0D3C30A1A011EF10000D49E /* AppSettings.h */, + F0D3C30B1A011EF10000D49E /* AppSettings.m */, + F03C470F1A02952800E445AB /* CustomAlert.h */, + F03C47101A02952800E445AB /* CustomAlert.m */, + F05B955D19DEED8A008761B0 /* MatrixHandler.h */, + F05B955E19DEED8A008761B0 /* MatrixHandler.m */, + F03EF60019F19E7C00A0EE52 /* MediaManager.h */, + F03EF60119F19E7C00A0EE52 /* MediaManager.m */, + F07A80E219DD9DE700B621A1 /* Main.storyboard */, + F07A80E519DD9DE700B621A1 /* Images.xcassets */, + F07A80E719DD9DE700B621A1 /* LaunchScreen.xib */, + F01628B519E298710071C473 /* Assets */, + F07A80D519DD9DE700B621A1 /* Supporting Files */, + ); + path = syMessaging; + sourceTree = ""; + }; + F07A80D519DD9DE700B621A1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + F07A80D619DD9DE700B621A1 /* Info.plist */, + F07A80D719DD9DE700B621A1 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + F07A80F119DD9DE700B621A1 /* syMessagingTests */ = { + isa = PBXGroup; + children = ( + F07A80F419DD9DE700B621A1 /* syMessagingTests.m */, + F07A80F219DD9DE700B621A1 /* Supporting Files */, + ); + path = syMessagingTests; + sourceTree = ""; + }; + F07A80F219DD9DE700B621A1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + F07A80F319DD9DE700B621A1 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F07A80D119DD9DE700B621A1 /* matrixConsole */ = { + isa = PBXNativeTarget; + buildConfigurationList = F07A80F819DD9DE700B621A1 /* Build configuration list for PBXNativeTarget "matrixConsole" */; + buildPhases = ( + A063750719371855C7755702 /* Check Pods Manifest.lock */, + F07A80CE19DD9DE700B621A1 /* Sources */, + F07A80CF19DD9DE700B621A1 /* Frameworks */, + F07A80D019DD9DE700B621A1 /* Resources */, + 3496B335A95D95AB2A8DCEF4 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = matrixConsole; + productName = syMessaging; + productReference = F07A80D219DD9DE700B621A1 /* matrixConsole.app */; + productType = "com.apple.product-type.application"; + }; + F07A80ED19DD9DE700B621A1 /* matrixConsoleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F07A80FB19DD9DE700B621A1 /* Build configuration list for PBXNativeTarget "matrixConsoleTests" */; + buildPhases = ( + F07A80EA19DD9DE700B621A1 /* Sources */, + F07A80EB19DD9DE700B621A1 /* Frameworks */, + F07A80EC19DD9DE700B621A1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F07A80F019DD9DE700B621A1 /* PBXTargetDependency */, + ); + name = matrixConsoleTests; + productName = syMessagingTests; + productReference = F07A80EE19DD9DE700B621A1 /* matrixConsoleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F07A80CA19DD9DE700B621A1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = matrix.org; + TargetAttributes = { + F07A80D119DD9DE700B621A1 = { + CreatedOnToolsVersion = 6.0; + }; + F07A80ED19DD9DE700B621A1 = { + CreatedOnToolsVersion = 6.0; + TestTargetID = F07A80D119DD9DE700B621A1; + }; + }; + }; + buildConfigurationList = F07A80CD19DD9DE700B621A1 /* Build configuration list for PBXProject "matrixConsole" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F07A80C919DD9DE700B621A1; + productRefGroup = F07A80D319DD9DE700B621A1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F07A80D119DD9DE700B621A1 /* matrixConsole */, + F07A80ED19DD9DE700B621A1 /* matrixConsoleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F07A80D019DD9DE700B621A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F024098219E7D177006E741B /* tab_recents@2x.png in Resources */, + F02BCE231A1A5A2B00543B47 /* play.png in Resources */, + F07A80E419DD9DE700B621A1 /* Main.storyboard in Resources */, + F07A80E919DD9DE700B621A1 /* LaunchScreen.xib in Resources */, + F0CEA5AF19E6895E00E47915 /* tab_recents.png in Resources */, + F01628C319E29C660071C473 /* logo.png in Resources */, + F01628C119E29C660071C473 /* default-profile.png in Resources */, + F0CEA5AE19E6895E00E47915 /* logoHighRes.png in Resources */, + F0CEA5B119E6898800E47915 /* tab_home.ico in Resources */, + F07A80E619DD9DE700B621A1 /* Images.xcassets in Resources */, + F08B6FCC1A1DE7F80094A35B /* matrixConsole.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F07A80EC19DD9DE700B621A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3496B335A95D95AB2A8DCEF4 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-matrixConsole/Pods-matrixConsole-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + A063750719371855C7755702 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F07A80CE19DD9DE700B621A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F07A80DB19DD9DE700B621A1 /* AppDelegate.m in Sources */, + F03EF5FF19F1762000A0EE52 /* RoomMessageTableCell.m in Sources */, + F07A80D819DD9DE700B621A1 /* main.m in Sources */, + F05B955F19DEED8A008761B0 /* MatrixHandler.m in Sources */, + F03EF5FB19F171EB00A0EE52 /* SettingsViewController.m in Sources */, + F03EF5FA19F171EB00A0EE52 /* RoomViewController.m in Sources */, + F03EF5F819F171EB00A0EE52 /* MasterTabBarController.m in Sources */, + F03EF5F619F171EB00A0EE52 /* HomeViewController.m in Sources */, + F03EF60219F19E7C00A0EE52 /* MediaManager.m in Sources */, + F03EF5F919F171EB00A0EE52 /* RecentsViewController.m in Sources */, + F03C47111A02952800E445AB /* CustomAlert.m in Sources */, + F0E84D401A1F9AEC005F2E42 /* RecentsTableViewCell.m in Sources */, + F02D707619F1DC9E007B47D3 /* RoomMemberTableCell.m in Sources */, + F00B5DB91A1B9BCE00EA1C8D /* CustomImageView.m in Sources */, + F0D3C30C1A011EF10000D49E /* AppSettings.m in Sources */, + F03EF5F719F171EB00A0EE52 /* LoginViewController.m in Sources */, + F0D3C30F1A01330F0000D49E /* SettingsTableViewCell.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F07A80EA19DD9DE700B621A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F07A80F519DD9DE700B621A1 /* syMessagingTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F07A80F019DD9DE700B621A1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F07A80D119DD9DE700B621A1 /* matrixConsole */; + targetProxy = F07A80EF19DD9DE700B621A1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + F07A80E219DD9DE700B621A1 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F07A80E319DD9DE700B621A1 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + F07A80E719DD9DE700B621A1 /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + F07A80E819DD9DE700B621A1 /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F07A80F619DD9DE700B621A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F07A80F719DD9DE700B621A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F07A80F919DD9DE700B621A1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B7EC7E45C718BF2BBCE0CF48 /* Pods-matrixConsole.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = syMessaging/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = matrixConsole; + }; + name = Debug; + }; + F07A80FA19DD9DE700B621A1 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 13057A57E74FD5504196F47F /* Pods-matrixConsole.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = syMessaging/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = matrixConsole; + }; + name = Release; + }; + F07A80FC19DD9DE700B621A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = syMessagingTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = matrixConsoleTests; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/matrixConsole.app/matrixConsole"; + }; + name = Debug; + }; + F07A80FD19DD9DE700B621A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = syMessagingTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = matrixConsoleTests; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/matrixConsole.app/matrixConsole"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F07A80CD19DD9DE700B621A1 /* Build configuration list for PBXProject "matrixConsole" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F07A80F619DD9DE700B621A1 /* Debug */, + F07A80F719DD9DE700B621A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F07A80F819DD9DE700B621A1 /* Build configuration list for PBXNativeTarget "matrixConsole" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F07A80F919DD9DE700B621A1 /* Debug */, + F07A80FA19DD9DE700B621A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F07A80FB19DD9DE700B621A1 /* Build configuration list for PBXNativeTarget "matrixConsoleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F07A80FC19DD9DE700B621A1 /* Debug */, + F07A80FD19DD9DE700B621A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F07A80CA19DD9DE700B621A1 /* Project object */; +} diff --git a/matrixConsole/AppDelegate.h b/matrixConsole/AppDelegate.h new file mode 100644 index 000000000..5a7b9c905 --- /dev/null +++ b/matrixConsole/AppDelegate.h @@ -0,0 +1,36 @@ +/* + 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 + +#import "MasterTabBarController.h" +#import "CustomAlert.h" + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) MasterTabBarController *masterTabBarController; + +@property (strong, nonatomic) CustomAlert *errorNotification; + ++ (AppDelegate*)theDelegate; + +- (void)logout; + +- (CustomAlert*)showErrorAsAlert:(NSError*)error; + +@end + diff --git a/matrixConsole/AppDelegate.m b/matrixConsole/AppDelegate.m new file mode 100644 index 000000000..8e570a559 --- /dev/null +++ b/matrixConsole/AppDelegate.m @@ -0,0 +1,133 @@ +/* + 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 "AppDelegate.h" +#import "AppSettings.h" +#import "RoomViewController.h" +#import "MatrixHandler.h" +#import "MediaManager.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +#pragma mark - + ++ (AppDelegate*)theDelegate { + return (AppDelegate*)[[UIApplication sharedApplication] delegate]; +} +#pragma mark - + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + if ([self.window.rootViewController isKindOfClass:[MasterTabBarController class]]) + { + self.masterTabBarController = (MasterTabBarController*)self.window.rootViewController; + self.masterTabBarController.delegate = self; + + // By default the "Home" tab is focussed + [self.masterTabBarController setSelectedIndex:TABBAR_HOME_INDEX]; + + UIViewController* recents = [self.masterTabBarController.viewControllers objectAtIndex:TABBAR_RECENTS_INDEX]; + if ([recents isKindOfClass:[UISplitViewController class]]) { + UISplitViewController *splitViewController = (UISplitViewController *)recents; + UINavigationController *navigationController = [splitViewController.viewControllers lastObject]; + navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem; + splitViewController.delegate = self; + } else { + // Patch missing image in tabBarItem for iOS < 8.0 + recents.tabBarItem.image = [[UIImage imageNamed:@"tab_recents"] imageWithRenderingMode:UIImageRenderingModeAutomatic]; + } + } + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (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. + + if (self.errorNotification) { + [self.errorNotification dismiss:NO]; + self.errorNotification = nil; + } +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +#pragma mark - + +- (void)logout { + // Clear cache + [MediaManager clearCache]; + // Reset App settings + [[AppSettings sharedSettings] reset]; + // Logout Matrix + [[MatrixHandler sharedHandler] logout]; + [self.masterTabBarController showLoginScreen]; + // By default the "Home" tab is focussed + [self.masterTabBarController setSelectedIndex:TABBAR_HOME_INDEX]; +} + +- (CustomAlert*)showErrorAsAlert:(NSError*)error { + if (self.errorNotification) { + [self.errorNotification dismiss:NO]; + } + + NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey]; + if (!title) + { + title = @"Error"; + } + NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey]; + + self.errorNotification = [[CustomAlert alloc] initWithTitle:title message:msg style:CustomAlertStyleAlert]; + self.errorNotification.cancelButtonIndex = [self.errorNotification addActionWithTitle:@"OK" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + [AppDelegate theDelegate].errorNotification = nil; + }]; + [self.errorNotification showInViewController:[self.masterTabBarController selectedViewController]]; + + return self.errorNotification; +} + +#pragma mark - Split view + +- (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)) { + // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. + return YES; + } else { + return NO; + } +} + +@end diff --git a/matrixConsole/AppSettings.h b/matrixConsole/AppSettings.h new file mode 100644 index 000000000..d79184e96 --- /dev/null +++ b/matrixConsole/AppSettings.h @@ -0,0 +1,29 @@ +/* + Copyright 2014 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#import + +@interface AppSettings : NSObject + +@property (nonatomic) BOOL enableNotifications; +@property (nonatomic) BOOL displayAllEvents; +@property (nonatomic) BOOL hideUnsupportedMessages; +@property (nonatomic) BOOL sortMembersUsingLastSeenTime; + ++ (AppSettings *)sharedSettings; + +- (void)reset; + +@end diff --git a/matrixConsole/AppSettings.m b/matrixConsole/AppSettings.m new file mode 100644 index 000000000..b3a0af719 --- /dev/null +++ b/matrixConsole/AppSettings.m @@ -0,0 +1,89 @@ +/* + 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 "AppSettings.h" +#import "MatrixHandler.h" + +static AppSettings *sharedSettings = nil; + +@implementation AppSettings + ++ (AppSettings *)sharedSettings { + @synchronized(self) { + if(sharedSettings == nil) + { + sharedSettings = [[super allocWithZone:NULL] init]; + } + } + return sharedSettings; +} + +#pragma mark - + +-(AppSettings *)init { + if (self = [super init]) { + } + return self; +} + +- (void)dealloc { +} + +- (void)reset { + self.enableNotifications = NO; + self.displayAllEvents = NO; + self.hideUnsupportedMessages = NO; + self.sortMembersUsingLastSeenTime = NO; +} + +#pragma mark - + +- (BOOL)enableNotifications { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"enableNotifications"]; +} + +- (void)setEnableNotifications:(BOOL)notifications { + [[MatrixHandler sharedHandler] enableEventsNotifications:notifications]; + [[NSUserDefaults standardUserDefaults] setBool:notifications forKey:@"enableNotifications"]; +} + +- (BOOL)displayAllEvents { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"displayAllEvents"]; +} + +- (void)setDisplayAllEvents:(BOOL)displayAllEvents { + [[NSUserDefaults standardUserDefaults] setBool:displayAllEvents forKey:@"displayAllEvents"]; + // Flush and restore Matrix data + [[MatrixHandler sharedHandler] forceInitialSync]; +} + +- (BOOL)hideUnsupportedMessages { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"hideUnsupportedMessages"]; +} + +- (void)setHideUnsupportedMessages:(BOOL)hideUnsupportedMessages { + [[NSUserDefaults standardUserDefaults] setBool:hideUnsupportedMessages forKey:@"hideUnsupportedMessages"]; +} + +- (BOOL)sortMembersUsingLastSeenTime { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"sortMembersUsingLastSeenTime"]; +} + +- (void)setSortMembersUsingLastSeenTime:(BOOL)sortMembersUsingLastSeen { + [[NSUserDefaults standardUserDefaults] setBool:sortMembersUsingLastSeen forKey:@"sortMembersUsingLastSeenTime"]; +} + +@end diff --git a/matrixConsole/Assets/close.png b/matrixConsole/Assets/close.png new file mode 100644 index 000000000..fbcdb51e6 Binary files /dev/null and b/matrixConsole/Assets/close.png differ diff --git a/matrixConsole/Assets/default-profile.png b/matrixConsole/Assets/default-profile.png new file mode 100644 index 000000000..6f81a3c41 Binary files /dev/null and b/matrixConsole/Assets/default-profile.png differ diff --git a/matrixConsole/Assets/gradient.png b/matrixConsole/Assets/gradient.png new file mode 100644 index 000000000..8ac9e2193 Binary files /dev/null and b/matrixConsole/Assets/gradient.png differ diff --git a/matrixConsole/Assets/logo.png b/matrixConsole/Assets/logo.png new file mode 100644 index 000000000..411206dcd Binary files /dev/null and b/matrixConsole/Assets/logo.png differ diff --git a/matrixConsole/Assets/logoHighRes.png b/matrixConsole/Assets/logoHighRes.png new file mode 100644 index 000000000..c4b53a848 Binary files /dev/null and b/matrixConsole/Assets/logoHighRes.png differ diff --git a/matrixConsole/Assets/matrixConsole.jpg b/matrixConsole/Assets/matrixConsole.jpg new file mode 100644 index 000000000..3d17aa9f8 Binary files /dev/null and b/matrixConsole/Assets/matrixConsole.jpg differ diff --git a/matrixConsole/Assets/play.png b/matrixConsole/Assets/play.png new file mode 100755 index 000000000..d93e82419 Binary files /dev/null and b/matrixConsole/Assets/play.png differ diff --git a/matrixConsole/Assets/tab_home.ico b/matrixConsole/Assets/tab_home.ico new file mode 100644 index 000000000..ba193fabc Binary files /dev/null and b/matrixConsole/Assets/tab_home.ico differ diff --git a/matrixConsole/Assets/tab_recents.png b/matrixConsole/Assets/tab_recents.png new file mode 100644 index 000000000..005b2265e Binary files /dev/null and b/matrixConsole/Assets/tab_recents.png differ diff --git a/matrixConsole/Assets/tab_recents@2x.png b/matrixConsole/Assets/tab_recents@2x.png new file mode 100644 index 000000000..005b2265e Binary files /dev/null and b/matrixConsole/Assets/tab_recents@2x.png differ diff --git a/matrixConsole/Base.lproj/LaunchScreen.xib b/matrixConsole/Base.lproj/LaunchScreen.xib new file mode 100644 index 000000000..fc64e2f3b --- /dev/null +++ b/matrixConsole/Base.lproj/LaunchScreen.xib @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matrixConsole/Base.lproj/Main.storyboard b/matrixConsole/Base.lproj/Main.storyboard new file mode 100644 index 000000000..482cbb7d8 --- /dev/null +++ b/matrixConsole/Base.lproj/Main.storyboard @@ -0,0 +1,1064 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matrixConsole/CustomAlert.h b/matrixConsole/CustomAlert.h new file mode 100644 index 000000000..79080c397 --- /dev/null +++ b/matrixConsole/CustomAlert.h @@ -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 + +// Note: UIAlertView is deprecated in iOS 8. To create and manage alerts in iOS 8 and later, instead use UIAlertController +// with a preferredStyle of UIAlertControllerStyleAlert. + +typedef enum : NSUInteger { + CustomAlertActionStyleDefault = 0, + CustomAlertActionStyleCancel, + CustomAlertActionStyleDestructive +} CustomAlertActionStyle; + +typedef enum : NSUInteger { + CustomAlertStyleActionSheet = 0, + CustomAlertStyleAlert +} CustomAlertStyle; + +@interface CustomAlert : NSObject { +} + +typedef void (^blockCustomAlert_onClick)(CustomAlert *alert); +typedef void (^blockCustomAlert_textFieldHandler)(UITextField *textField); + +@property(nonatomic) NSInteger cancelButtonIndex; // required to dismiss cusmtomAlert on iOS < 8 (default is -1). +@property(nonatomic, weak) UIView *sourceView; + +- (id)initWithTitle:(NSString *)title message:(NSString *)message style:(CustomAlertStyle)style; +// adds a button with the title. returns the index (0 based) of where it was added. +- (NSInteger)addActionWithTitle:(NSString *)title style:(CustomAlertActionStyle)style handler:(blockCustomAlert_onClick)handler; +// Adds a text field to an alert (Note: You can add a text field only if the style property is set to CustomAlertStyleAlert). +- (void)addTextFieldWithConfigurationHandler:(blockCustomAlert_textFieldHandler)configurationHandler; + +- (void)showInViewController:(UIViewController*)viewController; + +- (void)dismiss:(BOOL)animated; +- (UITextField *)textFieldAtIndex:(NSInteger)textFieldIndex; + +@end diff --git a/matrixConsole/CustomAlert.m b/matrixConsole/CustomAlert.m new file mode 100644 index 000000000..fc01ff877 --- /dev/null +++ b/matrixConsole/CustomAlert.m @@ -0,0 +1,210 @@ +/* + 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 "CustomAlert.h" + +#import + +@interface CustomAlert() +{ + UIViewController* parentViewController; + NSMutableArray *actions; // use only for iOS < 8 +} + +@property(nonatomic, strong) id alert; // alert is kind of UIAlertController for IOS 8 and later, in other cases it's kind of UIAlertView or UIActionSheet. +@end + +@implementation CustomAlert + +- (void)dealloc { + // iOS < 8 + if ([_alert isKindOfClass:[UIActionSheet class]] || [_alert isKindOfClass:[UIAlertView class]]) { + // Dismiss here AlertView or ActionSheet (if any) because its delegate is released + [self dismiss:NO]; + } + + _alert = nil; + parentViewController = nil; + actions = nil; +} + +- (id)initWithTitle:(NSString *)title message:(NSString *)message style:(CustomAlertStyle)style { + if (self = [super init]) { + // Check iOS version + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8) { + _alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:(UIAlertControllerStyle)style]; + } else { + // Use legacy objects + if (style == CustomAlertStyleActionSheet) { + _alert = [[UIActionSheet alloc] initWithTitle:title delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil]; + } else { + _alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:self cancelButtonTitle:nil otherButtonTitles:nil]; + } + + self.cancelButtonIndex = -1; + } + } + return self; +} + + +- (NSInteger)addActionWithTitle:(NSString *)title style:(CustomAlertActionStyle)style handler:(blockCustomAlert_onClick)handler { + NSInteger index = 0; + if ([_alert isKindOfClass:[UIAlertController class]]) { + index = [(UIAlertController *)_alert actions].count; + UIAlertAction* action = [UIAlertAction actionWithTitle:title + style:(UIAlertActionStyle)style + handler:^(UIAlertAction * action) { + if (handler) { + handler(self); + } + }]; + + [(UIAlertController *)_alert addAction:action]; + } else if ([_alert isKindOfClass:[UIActionSheet class]]) { + if (actions == nil) { + actions = [NSMutableArray array]; + } + index = [(UIActionSheet *)_alert addButtonWithTitle:title]; + if (handler) { + [actions addObject:handler]; + } else { + [actions addObject:[NSNull null]]; + } + } else if ([_alert isKindOfClass:[UIAlertView class]]) { + if (actions == nil) { + actions = [NSMutableArray array]; + } + index = [(UIAlertView *)_alert addButtonWithTitle:title]; + if (handler) { + [actions addObject:handler]; + } else { + [actions addObject:[NSNull null]]; + } + } + return index; +} + +- (void)addTextFieldWithConfigurationHandler:(blockCustomAlert_textFieldHandler)configurationHandler { + if ([_alert isKindOfClass:[UIAlertController class]]) { + [(UIAlertController *)_alert addTextFieldWithConfigurationHandler:configurationHandler]; + } else if ([_alert isKindOfClass:[UIAlertView class]]) { + UIAlertView *alertView = (UIAlertView *)_alert; + // Check the current style + if (alertView.alertViewStyle == UIAlertViewStyleDefault) { + // Add the first text fields + alertView.alertViewStyle = UIAlertViewStylePlainTextInput; + + if (configurationHandler) { + // Store the callback + UITextField *textField = [alertView textFieldAtIndex:0]; + objc_setAssociatedObject(textField, "configurationHandler", [configurationHandler copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + } else if (alertView.alertViewStyle != UIAlertViewStyleLoginAndPasswordInput) { + // Add a second text field + alertView.alertViewStyle = UIAlertViewStyleLoginAndPasswordInput; + + if (configurationHandler) { + // Store the callback + UITextField *textField = [alertView textFieldAtIndex:1]; + objc_setAssociatedObject(textField, "configurationHandler", [configurationHandler copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + } + // CAUTION 1: only 2 text fields are supported fro iOS < 8 + // CAUTION 2: alert style "UIAlertViewStyleSecureTextInput" is not supported, use the configurationHandler to handle secure text field + } +} + +- (void)showInViewController:(UIViewController*)viewController { + if ([_alert isKindOfClass:[UIAlertController class]]) { + if (viewController) { + parentViewController = viewController; + if (self.sourceView) { + [_alert popoverPresentationController].sourceView = self.sourceView; + [_alert popoverPresentationController].sourceRect = self.sourceView.bounds; + } + [viewController presentViewController:(UIAlertController *)_alert animated:YES completion:nil]; + } + } else if ([_alert isKindOfClass:[UIActionSheet class]]) { + [(UIActionSheet *)_alert showInView:[[UIApplication sharedApplication] keyWindow]]; + } else if ([_alert isKindOfClass:[UIAlertView class]]) { + UIAlertView *alertView = (UIAlertView *)_alert; + if (alertView.alertViewStyle != UIAlertViewStyleDefault) { + // Call here textField handlers + UITextField *textField = [alertView textFieldAtIndex:0]; + blockCustomAlert_textFieldHandler configurationHandler = objc_getAssociatedObject(textField, "configurationHandler"); + if (configurationHandler) { + configurationHandler (textField); + } + if (alertView.alertViewStyle == UIAlertViewStyleLoginAndPasswordInput) { + textField = [alertView textFieldAtIndex:1]; + blockCustomAlert_textFieldHandler configurationHandler = objc_getAssociatedObject(textField, "configurationHandler"); + if (configurationHandler) { + configurationHandler (textField); + } + } + } + [alertView show]; + } +} + +- (void)dismiss:(BOOL)animated { + if ([_alert isKindOfClass:[UIAlertController class]]) { + [parentViewController dismissViewControllerAnimated:animated completion:nil]; + } else if ([_alert isKindOfClass:[UIActionSheet class]]) { + [((UIActionSheet *)_alert) dismissWithClickedButtonIndex:self.cancelButtonIndex animated:animated]; + } else if ([_alert isKindOfClass:[UIAlertView class]]) { + [((UIAlertView *)_alert) dismissWithClickedButtonIndex:self.cancelButtonIndex animated:animated]; + } + _alert = nil; +} + +- (UITextField *)textFieldAtIndex:(NSInteger)textFieldIndex{ + if ([_alert isKindOfClass:[UIAlertController class]]) { + return [((UIAlertController*)_alert).textFields objectAtIndex:textFieldIndex]; + } else if ([_alert isKindOfClass:[UIAlertView class]]) { + return [((UIAlertView*)_alert) textFieldAtIndex:textFieldIndex]; + } + return nil; +} + +#pragma mark - UIAlertViewDelegate (iOS < 8) + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + // Retrieve the callback + blockCustomAlert_onClick block = [actions objectAtIndex:buttonIndex]; + if ([block isEqual:[NSNull null]] == NO) { + // And call it + block(self); + } + // Release alert reference + _alert = nil; +} + +#pragma mark - UIActionSheetDelegate (iOS < 8) + +- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { + // Retrieve the callback + blockCustomAlert_onClick block = [actions objectAtIndex:buttonIndex]; + if ([block isEqual:[NSNull null]] == NO) { + // And call it + block(self); + } + // Release _alert reference + _alert = nil; +} + +@end diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Contents.json b/matrixConsole/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..65501812a --- /dev/null +++ b/matrixConsole/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon-57.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon-57@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-29-1.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-29@2x-1.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-50.png", + "scale" : "1x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-50@2x.png", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29-1.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29-1.png new file mode 100644 index 000000000..31fcb2c66 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29-1.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29.png new file mode 100644 index 000000000..31fcb2c66 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@2x-1.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@2x-1.png new file mode 100644 index 000000000..969a78955 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@2x-1.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png new file mode 100644 index 000000000..969a78955 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png new file mode 100644 index 000000000..68bce94d3 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40.png new file mode 100644 index 000000000..0a3b097fd Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png new file mode 100644 index 000000000..712e94b11 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png new file mode 100644 index 000000000..712e94b11 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png new file mode 100644 index 000000000..9e551a276 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-50.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-50.png new file mode 100644 index 000000000..58eefab92 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-50.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-50@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-50@2x.png new file mode 100644 index 000000000..dc80fdb77 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-50@2x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-57.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-57.png new file mode 100644 index 000000000..65b87e847 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-57.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-57@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-57@2x.png new file mode 100644 index 000000000..8f09a3dbb Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-57@2x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 000000000..9e551a276 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 000000000..881660c8e Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-72.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-72.png new file mode 100644 index 000000000..fbd0de55d Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-72.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png new file mode 100644 index 000000000..2ba37c384 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-76.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 000000000..e5950c104 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 000000000..e30f4ad33 Binary files /dev/null and b/matrixConsole/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Contents.json b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 000000000..df7d17841 --- /dev/null +++ b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,107 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "launch@2x-1.png", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "launch-568h@2x-1.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Portrait~ipad.png", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Landscape~ipad.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Portrait@2x~ipad.png", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "Default-Landscape@2x~ipad.png", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "filename" : "launch.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "filename" : "launch@2x.png", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "filename" : "launch-568h@2x.png", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "Default-Portrait.png", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "Default-Landscape.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "Default-Portrait@2x.png", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "Default-Landscape@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape.png new file mode 100644 index 000000000..442664414 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png new file mode 100644 index 000000000..b7c65c976 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x~ipad.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x~ipad.png new file mode 100644 index 000000000..c671ee422 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape@2x~ipad.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape~ipad.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape~ipad.png new file mode 100644 index 000000000..9b9653870 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Landscape~ipad.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait.png new file mode 100644 index 000000000..57953fff3 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png new file mode 100644 index 000000000..3d8918213 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x~ipad.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x~ipad.png new file mode 100644 index 000000000..6f23fa9d4 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait@2x~ipad.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait~ipad.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait~ipad.png new file mode 100644 index 000000000..7bde9791b Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/Default-Portrait~ipad.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch-568h@2x-1.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch-568h@2x-1.png new file mode 100644 index 000000000..fa589947e Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch-568h@2x-1.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch-568h@2x.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch-568h@2x.png new file mode 100644 index 000000000..fa589947e Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch-568h@2x.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch.png new file mode 100644 index 000000000..d7a4df49f Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch@2x-1.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch@2x-1.png new file mode 100644 index 000000000..b8db7b641 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch@2x-1.png differ diff --git a/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch@2x.png b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch@2x.png new file mode 100644 index 000000000..b8db7b641 Binary files /dev/null and b/matrixConsole/Images.xcassets/LaunchImage.launchimage/launch@2x.png differ diff --git a/matrixConsole/Info.plist b/matrixConsole/Info.plist new file mode 100644 index 000000000..fda320dd4 --- /dev/null +++ b/matrixConsole/Info.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.matrix.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/matrixConsole/MatrixHandler.h b/matrixConsole/MatrixHandler.h new file mode 100644 index 000000000..36a715503 --- /dev/null +++ b/matrixConsole/MatrixHandler.h @@ -0,0 +1,52 @@ +/* + 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 + +extern NSString *const kMatrixHandlerUnsupportedMessagePrefix; + +@interface MatrixHandler : NSObject + +@property (strong, nonatomic) MXRestClient *mxRestClient; +@property (strong, nonatomic) MXSession *mxSession; + +@property (strong, nonatomic) NSString *homeServerURL; +@property (strong, nonatomic) NSString *homeServer; +@property (strong, nonatomic) NSString *userLogin; +@property (strong, nonatomic) NSString *userId; +@property (strong, nonatomic) NSString *accessToken; + +// Matrix user's settings +@property (strong, nonatomic) NSString *userDisplayName; +@property (strong, nonatomic) NSString *userPictureURL; + +@property (nonatomic,readonly) BOOL isLogged; +@property (nonatomic,readonly) BOOL isInitialSyncDone; + ++ (MatrixHandler *)sharedHandler; + +- (void)logout; + +// Flush and restore Matrix data +- (void)forceInitialSync; + +- (void)enableEventsNotifications:(BOOL)isEnabled; + +- (BOOL)isAttachment:(MXEvent*)message; +- (BOOL)isNotification:(MXEvent*)message; +- (NSString*)displayTextFor:(MXEvent*)message inSubtitleMode:(BOOL)isSubtitle; + +@end diff --git a/matrixConsole/MatrixHandler.m b/matrixConsole/MatrixHandler.m new file mode 100644 index 000000000..81f29d3e1 --- /dev/null +++ b/matrixConsole/MatrixHandler.m @@ -0,0 +1,629 @@ +/* + 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 "MatrixHandler.h" +#import "AppDelegate.h" +#import "AppSettings.h" +#import "CustomAlert.h" + +NSString *const kMatrixHandlerUnsupportedMessagePrefix = @"UNSUPPORTED MSG: "; + +static MatrixHandler *sharedHandler = nil; + +@interface MatrixHandler () { + // We will notify user only once on session failure + BOOL notifyOpenSessionFailure; + + // Handle user's settings change + id roomMembersListener; + // Handle events notification + id eventsListener; +} + +@property (nonatomic,readwrite) BOOL isInitialSyncDone; +@property (strong, nonatomic) CustomAlert *mxNotification; + +@end + +@implementation MatrixHandler + +@synthesize homeServerURL, homeServer, userLogin, userId, accessToken; +@synthesize userDisplayName, userPictureURL; + ++ (MatrixHandler *)sharedHandler { + @synchronized(self) { + if(sharedHandler == nil) + { + sharedHandler = [[super allocWithZone:NULL] init]; + } + } + return sharedHandler; +} + +#pragma mark - + +-(MatrixHandler *)init { + if (self = [super init]) { + _isInitialSyncDone = NO; + notifyOpenSessionFailure = YES; + + // Read potential homeserver url in shared defaults object + if (self.homeServerURL) { + self.mxRestClient = [[MXRestClient alloc] initWithHomeServer:self.homeServerURL]; + + if (self.accessToken) { + [self openSession]; + } + } + // The app will look for user's display name in incoming messages, it must not be nil. + if (self.userDisplayName == nil) { + self.userDisplayName = @""; + } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; + } + return self; +} + +- (void)openSession { + + MXCredentials *credentials = [[MXCredentials alloc] init]; + credentials.homeServer = self.homeServerURL; + credentials.userId = self.userId; + credentials.accessToken = self.accessToken; + + self.mxRestClient = [[MXRestClient alloc] initWithCredentials:credentials]; + if (self.mxRestClient) { + // Request user's display name + [self.mxRestClient displayNameForUser:self.userId success:^(NSString *displayname) { + self.userDisplayName = displayname; + } failure:^(NSError *error) { + NSLog(@"Get displayName failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + // Request user's avatar + [self.mxRestClient avatarUrlForUser:self.userId success:^(NSString *avatar_url) { + self.userPictureURL = avatar_url; + } failure:^(NSError *error) { + NSLog(@"Get picture url failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + + self.mxSession = [[MXSession alloc] initWithMatrixRestClient:self.mxRestClient]; + // Check here whether the app user wants to display all the events + if ([[AppSettings sharedSettings] displayAllEvents]) { + // Override events filter to retrieve all the events + self.mxSession.eventsFilterForMessages = @[ + kMXEventTypeStringRoomName, + kMXEventTypeStringRoomTopic, + kMXEventTypeStringRoomMember, + kMXEventTypeStringRoomCreate, + kMXEventTypeStringRoomJoinRules, + kMXEventTypeStringRoomPowerLevels, + kMXEventTypeStringRoomAddStateLevel, + kMXEventTypeStringRoomSendEventLevel, + kMXEventTypeStringRoomOpsLevel, + kMXEventTypeStringRoomAliases, + kMXEventTypeStringRoomMessage, + kMXEventTypeStringRoomMessageFeedback, + kMXEventTypeStringPresence + ]; + } + // Launch mxSession + [self.mxSession start:^{ + self.isInitialSyncDone = YES; + + // Register listener to update user's information + roomMembersListener = [self.mxSession listenToEventsOfTypes:@[kMXEventTypeStringPresence] onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) { + // Consider only live events + if (direction == MXEventDirectionForwards) { + // Consider only events from app user + if ([event.userId isEqualToString:self.userId]) { + // Update local storage + if (![self.userDisplayName isEqualToString:event.content[@"displayname"]]) { + self.userDisplayName = event.content[@"displayname"]; + } + if (![self.userPictureURL isEqualToString:event.content[@"avatar_url"]]) { + self.userPictureURL = event.content[@"avatar_url"]; + } + } + } + }]; + + // Check whether the app user wants notifications on new events + if ([[AppSettings sharedSettings] enableNotifications]) { + [self enableEventsNotifications:YES]; + } + } failure:^(NSError *error) { + NSLog(@"Initial Sync failed: %@", error); + if (notifyOpenSessionFailure) { + //Alert user only once + notifyOpenSessionFailure = NO; + [[AppDelegate theDelegate] showErrorAsAlert:error]; + } + + // Postpone a new attempt in 10 sec + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self openSession]; + }); + }]; + } +} + +- (void)closeSession { + if (eventsListener) { + [self.mxSession removeListener:eventsListener]; + eventsListener = nil; + } + if (roomMembersListener) { + [self.mxSession removeListener:roomMembersListener]; + roomMembersListener = nil; + } + [self.mxSession close]; + self.mxSession = nil; + + [self.mxRestClient close]; + if (self.homeServerURL) { + self.mxRestClient = [[MXRestClient alloc] initWithHomeServer:self.homeServerURL]; + } else { + self.mxRestClient = nil; + } + + self.isInitialSyncDone = NO; + notifyOpenSessionFailure = YES; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [self closeSession]; + self.mxSession = nil; + + if (self.mxNotification) { + [self.mxNotification dismiss:NO]; + self.mxNotification = nil; + } +} + +- (void)onAppDidEnterBackground { + // Hide potential notification + if (self.mxNotification) { + [self.mxNotification dismiss:NO]; + self.mxNotification = nil; + } +} + +#pragma mark - + +- (BOOL)isLogged { + return (self.accessToken != nil); +} + +- (void)logout { + // Reset access token (mxSession is closed by setter) + self.accessToken = nil; + + // Reset local storage of user's settings + self.userDisplayName = @""; + self.userPictureURL = nil; +} + +- (void)forceInitialSync { + [self closeSession]; + notifyOpenSessionFailure = NO; + if (self.accessToken) { + [self openSession]; + } +} + +- (void)enableEventsNotifications:(BOOL)isEnabled { + if (isEnabled) { + // Register events listener + eventsListener = [self.mxSession listenToEventsOfTypes:self.mxSession.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) { + // Consider only live event (Ignore presence event) + if (direction == MXEventDirectionForwards && (event.eventType != MXEventTypePresence)) { + // If we are running on background, show a local notif + if (UIApplicationStateBackground == [UIApplication sharedApplication].applicationState) + { + UILocalNotification *localNotification = [[UILocalNotification alloc] init]; + localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:0]; + localNotification.hasAction = YES; + [localNotification setAlertBody:[self displayTextFor:event inSubtitleMode:YES]]; + [[UIApplication sharedApplication] scheduleLocalNotification:localNotification]; + } else if ([[AppDelegate theDelegate].masterTabBarController.visibleRoomId isEqualToString:event.roomId] == NO) { + // 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:[self.mxSession room:event.roomId].state.displayname + message:[self displayTextFor:event 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]]; + } + } + }]; + } else { + if (eventsListener) { + [self.mxSession removeListener:eventsListener]; + eventsListener = nil; + } + if (self.mxNotification) { + [self.mxNotification dismiss:NO]; + self.mxNotification = nil; + } + } +} + +#pragma mark - Properties + +- (NSString *)homeServerURL { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"homeserverurl"]; +} + +- (void)setHomeServerURL:(NSString *)inHomeserverURL { + if (inHomeserverURL.length) { + [[NSUserDefaults standardUserDefaults] setObject:inHomeserverURL forKey:@"homeserverurl"]; + self.mxRestClient = [[MXRestClient alloc] initWithHomeServer:inHomeserverURL]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"homeserverurl"]; + // Reinitialize matrix handler + [self logout]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)homeServer { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"homeserver"]; +} + +- (void)setHomeServer:(NSString *)inHomeserver { + if (inHomeserver.length) { + [[NSUserDefaults standardUserDefaults] setObject:inHomeserver forKey:@"homeserver"]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"homeserver"]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)userLogin { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"userlogin"]; +} + +- (void)setUserLogin:(NSString *)inUserLogin { + if (inUserLogin.length) { + [[NSUserDefaults standardUserDefaults] setObject:inUserLogin forKey:@"userlogin"]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"userlogin"]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)userId { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"userid"]; +} + +- (void)setUserId:(NSString *)inUserId { + if (inUserId.length) { + [[NSUserDefaults standardUserDefaults] setObject:inUserId forKey:@"userid"]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"userid"]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)accessToken { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"accesstoken"]; +} + +- (void)setAccessToken:(NSString *)inAccessToken { + if (inAccessToken.length) { + [[NSUserDefaults standardUserDefaults] setObject:inAccessToken forKey:@"accesstoken"]; + [self openSession]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"accesstoken"]; + [self closeSession]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +#pragma mark - Matrix user's settings + +- (NSString *)userDisplayName { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"userdisplayname"]; +} + +- (void)setUserDisplayName:(NSString *)inUserDisplayName { + if ([inUserDisplayName isEqual:[NSNull null]] == NO && inUserDisplayName.length) { + [[NSUserDefaults standardUserDefaults] setObject:inUserDisplayName forKey:@"userdisplayname"]; + } else { + // the app will look for this display name in incoming messages, it must not be nil. + [[NSUserDefaults standardUserDefaults] setObject:@"" forKey:@"userdisplayname"]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSString *)userPictureURL { + return [[NSUserDefaults standardUserDefaults] objectForKey:@"userpictureurl"]; +} + +- (void)setUserPictureURL:(NSString *)inUserPictureURL { + if ([inUserPictureURL isEqual:[NSNull null]] == NO && inUserPictureURL.length) { + [[NSUserDefaults standardUserDefaults] setObject:inUserPictureURL forKey:@"userpictureurl"]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"userpictureurl"]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +#pragma mark - messages handler + +- (BOOL)isAttachment:(MXEvent*)message { + if (message.eventType == MXEventTypeRoomMessage) { + NSString *msgtype = message.content[@"msgtype"]; + if ([msgtype isEqualToString:kMXMessageTypeImage] + || [msgtype isEqualToString:kMXMessageTypeAudio] + || [msgtype isEqualToString:kMXMessageTypeVideo] + || [msgtype isEqualToString:kMXMessageTypeLocation]) { + return YES; + } + } + return NO; +} + +- (BOOL)isNotification:(MXEvent*)message { + // We consider as notification mxEvent which is not a text message or an attachment + if (message.eventType == MXEventTypeRoomMessage) { + NSString *msgtype = message.content[@"msgtype"]; + if ([msgtype isEqualToString:kMXMessageTypeEmote]) { + return YES; + } + return NO; + } + return YES; +} + +- (NSString*)displayTextFor:(MXEvent*)message inSubtitleMode:(BOOL)isSubtitle { + NSString *displayText = nil; + // Retrieve roomData related to the message + MXRoom *room = [self.mxSession room:message.roomId]; + // Prepare display name for concerned users + NSString *memberDisplayName = [room.state memberName:message.userId]; + NSString *targetDisplayName = nil; + if (message.stateKey) { + targetDisplayName = [room.state memberName:message.stateKey]; + } + + switch (message.eventType) { + case MXEventTypeRoomName: { + displayText = [NSString stringWithFormat:@"%@ changed the room name to: %@", memberDisplayName, message.content[@"name"]]; + break; + } + case MXEventTypeRoomTopic: { + displayText = [NSString stringWithFormat:@"%@ changed the topic to: %@", memberDisplayName, message.content[@"topic"]]; + break; + } + case MXEventTypeRoomMember: { + // Presently only change on membership, display name and avatar are supported + + // Retrieve membership + NSString* membership = message.content[@"membership"]; + NSString *prevMembership = nil; + if (message.prevContent) { + prevMembership = message.prevContent[@"membership"]; + } + + // Check whether the membership is unchanged + if (prevMembership && membership && [membership isEqualToString:prevMembership]) { + // Check whether the display name has been changed + NSString *displayname = message.content[@"displayname"]; + NSString *prevDisplayname = message.prevContent[@"displayname"]; + if (!displayname.length) { + displayname = nil; + } + if (!prevDisplayname.length) { + prevDisplayname = nil; + } + if ((displayname || prevDisplayname) && ([displayname isEqualToString:prevDisplayname] == NO)) { + displayText = [NSString stringWithFormat:@"%@ changed their display name from %@ to %@", message.userId, prevDisplayname, displayname]; + } + + // Check whether the avatar has been changed + NSString *avatar = message.content[@"avatar_url"]; + NSString *prevAvatar = message.prevContent[@"avatar_url"]; + if (!avatar.length) { + avatar = nil; + } + if (!prevAvatar.length) { + prevAvatar = nil; + } + if ((prevAvatar || avatar) && ([avatar isEqualToString:prevAvatar] == NO)) { + if (displayText) { + displayText = [NSString stringWithFormat:@"%@ (picture profile was changed too)", displayText]; + } else { + displayText = [NSString stringWithFormat:@"%@ changed their picture profile", memberDisplayName]; + } + } + } else { + // Consider here a membership change + if ([membership isEqualToString:@"invite"]) { + displayText = [NSString stringWithFormat:@"%@ invited %@", memberDisplayName, targetDisplayName]; + } else if ([membership isEqualToString:@"join"]) { + displayText = [NSString stringWithFormat:@"%@ joined", memberDisplayName]; + } else if ([membership isEqualToString:@"leave"]) { + if ([message.userId isEqualToString:message.stateKey]) { + displayText = [NSString stringWithFormat:@"%@ left", memberDisplayName]; + } else if (prevMembership) { + if ([prevMembership isEqualToString:@"join"] || [prevMembership isEqualToString:@"invite"]) { + displayText = [NSString stringWithFormat:@"%@ kicked %@", memberDisplayName, targetDisplayName]; + if (message.content[@"reason"]) { + displayText = [NSString stringWithFormat:@"%@: %@", displayText, message.content[@"reason"]]; + } + } else if ([prevMembership isEqualToString:@"ban"]) { + displayText = [NSString stringWithFormat:@"%@ unbanned %@", memberDisplayName, targetDisplayName]; + } + } + } else if ([membership isEqualToString:@"ban"]) { + displayText = [NSString stringWithFormat:@"%@ banned %@", memberDisplayName, targetDisplayName]; + if (message.content[@"reason"]) { + displayText = [NSString stringWithFormat:@"%@: %@", displayText, message.content[@"reason"]]; + } + } + } + break; + } + case MXEventTypeRoomCreate: { + NSString *creatorId = message.content[@"creator"]; + if (creatorId) { + displayText = [NSString stringWithFormat:@"%@ created the room", [room.state memberName:creatorId]]; + } + break; + } + case MXEventTypeRoomJoinRules: { + NSString *joinRule = message.content[@"join_rule"]; + if (joinRule) { + displayText = [NSString stringWithFormat:@"The join rule is: %@", joinRule]; + } + break; + } + case MXEventTypeRoomPowerLevels: { + displayText = @"The power level of room members are:"; + NSDictionary *users = message.content[@"users"]; + for (NSString *key in users.allKeys) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 %@: %@", displayText, key, [users objectForKey:key]]; + } + if (message.content[@"users_default"]) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 %@: %@", displayText, @"default", message.content[@"users_default"]]; + } + + displayText = [NSString stringWithFormat:@"%@\r\nThe minimum power levels that a user must have before acting are:", displayText]; + if (message.content[@"ban"]) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 ban: %@", displayText, message.content[@"ban"]]; + } + if (message.content[@"kick"]) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 kick: %@", displayText, message.content[@"kick"]]; + } + if (message.content[@"redact"]) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 redact: %@", displayText, message.content[@"redact"]]; + } + + displayText = [NSString stringWithFormat:@"%@\r\nThe minimum power levels related to events are:", displayText]; + NSDictionary *events = message.content[@"events"]; + for (NSString *key in events.allKeys) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 %@: %@", displayText, key, [events objectForKey:key]]; + } + if (message.content[@"events_default"]) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 %@: %@", displayText, @"events_default", message.content[@"events_default"]]; + } + if (message.content[@"state_default"]) { + displayText = [NSString stringWithFormat:@"%@\r\n\u2022 %@: %@", displayText, @"state_default", message.content[@"state_default"]]; + } + break; + } +// case MXEventTypeRoomAddStateLevel: { +// NSString *minLevel = message.content[@"level"]; +// if (minLevel) { +// displayText = [NSString stringWithFormat:@"The minimum power level a user needs to add state is: %@", minLevel]; +// } +// break; +// } +// case MXEventTypeRoomSendEventLevel: { +// NSString *minLevel = message.content[@"level"]; +// if (minLevel) { +// displayText = [NSString stringWithFormat:@"The minimum power level a user needs to send an event is: %@", minLevel]; +// } +// break; +// } +// case MXEventTypeRoomOpsLevel: { +// displayText = @"The minimum power levels that a user must have before acting are:"; +// for (NSString *key in message.content.allKeys) { +// displayText = [NSString stringWithFormat:@"%@\r\n%@:%@", displayText, key, [message.content objectForKey:key]]; +// } +// break; +// } + case MXEventTypeRoomAliases: { + NSArray *aliases = message.content[@"aliases"]; + if (aliases) { + displayText = [NSString stringWithFormat:@"The room aliases are: %@", aliases]; + } + break; + } + case MXEventTypeRoomMessage: { + NSString *msgtype = message.content[@"msgtype"]; + if ([msgtype isEqualToString:kMXMessageTypeText]) { + displayText = message.content[@"body"]; + } else if ([msgtype isEqualToString:kMXMessageTypeEmote]) { + displayText = [NSString stringWithFormat:@"* %@ %@", memberDisplayName, message.content[@"body"]]; + } else if ([msgtype isEqualToString:kMXMessageTypeImage]) { + displayText = @"image attachment"; + } else if ([msgtype isEqualToString:kMXMessageTypeAudio]) { + displayText = @"audio attachment"; + } else if ([msgtype isEqualToString:kMXMessageTypeVideo]) { + displayText = @"video attachment"; + } else if ([msgtype isEqualToString:kMXMessageTypeLocation]) { + displayText = @"location attachment"; + } + + // Check whether the sender name has to be added + if (isSubtitle && [msgtype isEqualToString:kMXMessageTypeEmote] == NO) { + displayText = [NSString stringWithFormat:@"%@: %@", memberDisplayName, displayText]; + } + + break; + } + case MXEventTypeRoomMessageFeedback: { + NSString *type = message.content[@"type"]; + NSString *eventId = message.content[@"target_event_id"]; + if (type && eventId) { + displayText = [NSString stringWithFormat:@"Feedback event (id: %@): %@", eventId, type]; + } + break; + } + case MXEventTypeCustom: + break; + default: + break; + } + + if (displayText == nil) { + NSLog(@"ERROR: Unsupported message %@)", message.description); + if (isSubtitle || [AppSettings sharedSettings].hideUnsupportedMessages) { + displayText = @""; + } else { + // Return event content as unsupported message + displayText = [NSString stringWithFormat:@"%@%@", kMatrixHandlerUnsupportedMessagePrefix, message.description]; + } + } + + return displayText; +} + +@end diff --git a/matrixConsole/MediaManager.h b/matrixConsole/MediaManager.h new file mode 100644 index 000000000..82852184c --- /dev/null +++ b/matrixConsole/MediaManager.h @@ -0,0 +1,50 @@ +/* + 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 +#import + +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 diff --git a/matrixConsole/MediaManager.m b/matrixConsole/MediaManager.m new file mode 100644 index 000000000..4fb111d02 --- /dev/null +++ b/matrixConsole/MediaManager.m @@ -0,0 +1,327 @@ +/* + 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 { + 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 diff --git a/matrixConsole/View/CustomImageView.h b/matrixConsole/View/CustomImageView.h new file mode 100644 index 000000000..9095727d5 --- /dev/null +++ b/matrixConsole/View/CustomImageView.h @@ -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 + +// Customize UIImageView in order to let UIImageView handle automatically remote url +@interface CustomImageView : UIImageView + +@property (strong, nonatomic) NSString *placeholder; +@property (strong, nonatomic) NSString *imageURL; +// Information about the media represented by this image (image, video...) +@property (strong, nonatomic) NSDictionary *mediaInfo; +@end + diff --git a/matrixConsole/View/CustomImageView.m b/matrixConsole/View/CustomImageView.m new file mode 100644 index 000000000..c231e8137 --- /dev/null +++ b/matrixConsole/View/CustomImageView.m @@ -0,0 +1,82 @@ +/* + 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" + +@interface CustomImageView () { + id imageLoader; + UIActivityIndicatorView *loadingWheel; +} +@end + +@implementation CustomImageView + +- (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) { + // Start loading animation + if (loadingWheel == nil) { + loadingWheel = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + CGPoint center = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2); + loadingWheel.center = center; + [self addSubview:loadingWheel]; + } + if ([self.backgroundColor isEqual:[UIColor blackColor]]) { + loadingWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite; + } else { + loadingWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; + } + [loadingWheel startAnimating]; + // Load picture + imageLoader = [MediaManager loadPicture:imageURL + success:^(UIImage *image) { + [loadingWheel stopAnimating]; + self.image = image; + } + failure:^(NSError *error) { + [loadingWheel stopAnimating]; + NSLog(@"Failed to download image (%@): %@", imageURL, error); + }]; + } +} + +- (void)dealloc { + if (imageLoader) { + [MediaManager cancel:imageLoader]; + imageLoader = nil; + } + if (loadingWheel) { + [loadingWheel removeFromSuperview]; + loadingWheel = nil; + } +} + +@end \ No newline at end of file diff --git a/matrixConsole/View/RecentsTableViewCell.h b/matrixConsole/View/RecentsTableViewCell.h new file mode 100644 index 000000000..27918556f --- /dev/null +++ b/matrixConsole/View/RecentsTableViewCell.h @@ -0,0 +1,25 @@ +/* + 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 + +@interface RecentsTableViewCell : UITableViewCell + +@property (weak, nonatomic) IBOutlet UILabel *roomTitle; +@property (weak, nonatomic) IBOutlet UILabel *lastEventDescription; +@property (weak, nonatomic) IBOutlet UILabel *recentDate; + +@end \ No newline at end of file diff --git a/matrixConsole/View/RecentsTableViewCell.m b/matrixConsole/View/RecentsTableViewCell.m new file mode 100644 index 000000000..e8d06d207 --- /dev/null +++ b/matrixConsole/View/RecentsTableViewCell.m @@ -0,0 +1,20 @@ +/* + 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 "RecentsTableViewCell.h" + +@implementation RecentsTableViewCell +@end \ No newline at end of file diff --git a/matrixConsole/View/RoomMemberTableCell.h b/matrixConsole/View/RoomMemberTableCell.h new file mode 100644 index 000000000..cda9dac6c --- /dev/null +++ b/matrixConsole/View/RoomMemberTableCell.h @@ -0,0 +1,32 @@ +/* + 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 +#import "CustomImageView.h" + +@class MXRoomMember; +@class MXRoom; + +// Room Member Table View Cell +@interface RoomMemberTableCell : UITableViewCell +@property (strong, nonatomic) IBOutlet CustomImageView *pictureView; +@property (weak, nonatomic) IBOutlet UILabel *userLabel; +@property (weak, nonatomic) IBOutlet UIProgressView *userPowerLevel; +@property (weak, nonatomic) IBOutlet UILabel *lastActiveAgoLabel; + +- (void)setRoomMember:(MXRoomMember *)roomMember withRoom:(MXRoom *)room; +@end + diff --git a/matrixConsole/View/RoomMemberTableCell.m b/matrixConsole/View/RoomMemberTableCell.m new file mode 100644 index 000000000..6cbcdfafa --- /dev/null +++ b/matrixConsole/View/RoomMemberTableCell.m @@ -0,0 +1,148 @@ +/* + 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 "RoomMemberTableCell.h" +#import "MatrixHandler.h" + +@implementation RoomMemberTableCell + +- (void)setRoomMember:(MXRoomMember *)roomMember withRoom:(MXRoom *)room { + if (room && roomMember) { + self.userLabel.text = [room.state memberName:roomMember.userId]; + self.pictureView.placeholder = @"default-profile"; + self.pictureView.imageURL = roomMember.avatarUrl; + // Round image view + [self.pictureView.layer setCornerRadius:self.pictureView.frame.size.width / 2]; + self.pictureView.clipsToBounds = YES; + + // Shade invited users + if (roomMember.membership == MXMembershipInvite) { + for (UIView *view in self.subviews) { + view.alpha = 0.3; + } + } else { + for (UIView *view in self.subviews) { + view.alpha = 1; + } + } + + // Customize banned and left (kicked) members + if (roomMember.membership == MXMembershipLeave || roomMember.membership == MXMembershipBan) { + self.backgroundColor = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1.0]; + + self.userPowerLevel.hidden = YES; + + self.lastActiveAgoLabel.backgroundColor = [UIColor lightGrayColor]; + self.lastActiveAgoLabel.text = (roomMember.membership == MXMembershipLeave) ? @"left" : @"banned"; + } else { + self.backgroundColor = [UIColor whiteColor]; + + // Handle power level display + self.userPowerLevel.hidden = NO; + NSDictionary *powerLevels; + if (room.state.powerLevels[@"users"]){ + // In Matrix 0.5, users power levels are listed under the `users` dictionnary + powerLevels = room.state.powerLevels[@"users"]; + } + else { + // @TODO: Remove this backward compatibility + powerLevels = room.state.powerLevels; + } + + if (powerLevels) { + int maxLevel = 0; + for (NSString *powerLevel in powerLevels.allValues) { + int level = [powerLevel intValue]; + if (level > maxLevel) { + maxLevel = level; + } + } + NSString *userPowerLevel = [powerLevels objectForKey:roomMember.userId]; // CAUTION: we invoke objectForKey here because user_id starts with an '@' character + if (userPowerLevel == nil) { + userPowerLevel = [powerLevels valueForKey:@"default"]; + } + float userPowerLevelFloat = 0.0; + if (userPowerLevel) { + userPowerLevelFloat = [userPowerLevel floatValue]; + } + self.userPowerLevel.progress = maxLevel ? userPowerLevelFloat / maxLevel : 1; + } else { + self.userPowerLevel.progress = 0; + } + + if (roomMember.membership == MXMembershipInvite) { + self.lastActiveAgoLabel.backgroundColor = [UIColor lightGrayColor]; + self.lastActiveAgoLabel.text = @"invited"; + } else { + // Get the user that corresponds to this member + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + MXUser *user = [mxHandler.mxSession user:roomMember.userId]; + + // Prepare last active ago string + NSUInteger lastActiveAgoInSec = user.lastActiveAgo / 1000; + NSString *lastActive; + if (lastActiveAgoInSec < 60) { + lastActive = [NSString stringWithFormat:@"%ds ago", lastActiveAgoInSec]; + } else if (lastActiveAgoInSec < 3600) { + lastActive = [NSString stringWithFormat:@"%dm ago", (lastActiveAgoInSec / 60)]; + } else if (lastActiveAgoInSec < 86400) { + lastActive = [NSString stringWithFormat:@"%dh ago", (lastActiveAgoInSec / 3600)]; + } else { + lastActive = [NSString stringWithFormat:@"%dd ago", (lastActiveAgoInSec / 86400)]; + } + + // Check presence + switch (user.presence) { + case MXPresenceUnknown: { + self.lastActiveAgoLabel.backgroundColor = [UIColor clearColor]; + self.lastActiveAgoLabel.text = nil;//@"unknown"; + break; + } + case MXPresenceOnline: { + self.lastActiveAgoLabel.backgroundColor = [UIColor colorWithRed:0.2 green:0.9 blue:0.2 alpha:1.0]; + self.lastActiveAgoLabel.text = lastActive; + self.lastActiveAgoLabel.numberOfLines = 0; + break; + } + case MXPresenceUnavailable: { + self.lastActiveAgoLabel.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.0 alpha:1.0]; + self.lastActiveAgoLabel.text = lastActive; + self.lastActiveAgoLabel.numberOfLines = 0; + break; + } + case MXPresenceOffline: { + self.lastActiveAgoLabel.backgroundColor = [UIColor colorWithRed:0.9 green:0.2 blue:0.2 alpha:1.0]; + self.lastActiveAgoLabel.text = @"offline"; + break; + } + case MXPresenceFreeForChat: { + self.lastActiveAgoLabel.backgroundColor = [UIColor clearColor]; + self.lastActiveAgoLabel.text = nil;//@"free for chat"; + break; + } + case MXPresenceHidden: { + self.lastActiveAgoLabel.backgroundColor = [UIColor clearColor]; + self.lastActiveAgoLabel.text = nil;//@"hidden"; + break; + } + default: + break; + } + } + } + } +} +@end \ No newline at end of file diff --git a/matrixConsole/View/RoomMessageTableCell.h b/matrixConsole/View/RoomMessageTableCell.h new file mode 100644 index 000000000..577fb8075 --- /dev/null +++ b/matrixConsole/View/RoomMessageTableCell.h @@ -0,0 +1,44 @@ +/* + 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 +#import "CustomImageView.h" + +// Room Message Table View Cell +@interface RoomMessageTableCell : UITableViewCell +@property (strong, nonatomic) IBOutlet CustomImageView *pictureView; +@property (weak, nonatomic) IBOutlet UITextView *messageTextView; +@property (strong, nonatomic) IBOutlet CustomImageView *attachmentView; +@property (strong, nonatomic) IBOutlet UIImageView *playIconView; +@property (weak, nonatomic) IBOutlet UILabel *dateTimeLabel; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *msgTextViewWidthConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *msgTextViewTopConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *msgTextViewBottomConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *dateTimeLabelTopConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachmentViewTopAlignmentConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachmentViewBottomAlignmentConstraint; +@end + +@interface IncomingMessageTableCell : RoomMessageTableCell +@property (weak, nonatomic) IBOutlet UILabel *userNameLabel; +@end + +@interface OutgoingMessageTableCell : RoomMessageTableCell +@property (weak, nonatomic) IBOutlet UILabel *unsentLabel; +@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *unsentLabelTopConstraint; +@end + diff --git a/matrixConsole/View/RoomMessageTableCell.m b/matrixConsole/View/RoomMessageTableCell.m new file mode 100644 index 000000000..733ef1306 --- /dev/null +++ b/matrixConsole/View/RoomMessageTableCell.m @@ -0,0 +1,29 @@ +/* + Copyright 2014 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "RoomMessageTableCell.h" +#import "MediaManager.h" + +@implementation RoomMessageTableCell +@end + + +@implementation IncomingMessageTableCell +@end + + +@implementation OutgoingMessageTableCell +@end \ No newline at end of file diff --git a/matrixConsole/View/SettingsTableViewCell.h b/matrixConsole/View/SettingsTableViewCell.h new file mode 100644 index 000000000..51f61299a --- /dev/null +++ b/matrixConsole/View/SettingsTableViewCell.h @@ -0,0 +1,29 @@ +/* + Copyright 2014 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@interface SettingsTableViewCell : UITableViewCell +@end + +@interface SettingsTableCellWithSwitch : SettingsTableViewCell +@property (strong, nonatomic) IBOutlet UILabel *settingLabel; +@property (strong, nonatomic) IBOutlet UISwitch *settingSwitch; +@end + +@interface SettingsTableCellWithTextView : SettingsTableViewCell +@property (strong, nonatomic) IBOutlet UITextView *settingTextView; +@end \ No newline at end of file diff --git a/matrixConsole/View/SettingsTableViewCell.m b/matrixConsole/View/SettingsTableViewCell.m new file mode 100644 index 000000000..74f11fba5 --- /dev/null +++ b/matrixConsole/View/SettingsTableViewCell.m @@ -0,0 +1,26 @@ +/* + 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 "SettingsTableViewCell.h" + +@implementation SettingsTableViewCell +@end + +@implementation SettingsTableCellWithSwitch +@end + +@implementation SettingsTableCellWithTextView +@end \ No newline at end of file diff --git a/matrixConsole/ViewController/HomeViewController.h b/matrixConsole/ViewController/HomeViewController.h new file mode 100644 index 000000000..de6fde18d --- /dev/null +++ b/matrixConsole/ViewController/HomeViewController.h @@ -0,0 +1,22 @@ +/* + 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 + +@interface HomeViewController : UITableViewController + +@end + diff --git a/matrixConsole/ViewController/HomeViewController.m b/matrixConsole/ViewController/HomeViewController.m new file mode 100644 index 000000000..b0796bf98 --- /dev/null +++ b/matrixConsole/ViewController/HomeViewController.m @@ -0,0 +1,386 @@ +/* + 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 "HomeViewController.h" + +#import "MatrixHandler.h" +#import "AppDelegate.h" + + + +@interface HomeViewController () { + NSArray *publicRooms; + // List of public room names to highlight in displayed list + NSArray* highlightedPublicRooms; +} + +@property (weak, nonatomic) IBOutlet UITableView *publicRoomsTable; +@property (weak, nonatomic) IBOutlet UILabel *roomCreationLabel; +@property (weak, nonatomic) IBOutlet UILabel *roomNameLabel; +@property (weak, nonatomic) IBOutlet UILabel *roomAliasLabel; +@property (weak, nonatomic) IBOutlet UILabel *participantsLabel; +@property (weak, nonatomic) IBOutlet UITextField *roomNameTextField; +@property (weak, nonatomic) IBOutlet UITextField *roomAliasTextField; +@property (weak, nonatomic) IBOutlet UITextField *participantsTextField; +@property (weak, nonatomic) IBOutlet UISegmentedControl *roomVisibilityControl; +@property (weak, nonatomic) IBOutlet UIButton *createRoomBtn; +- (IBAction)onButtonPressed:(id)sender; + +@end + +@implementation HomeViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Do any additional setup after loading the view, typically from a nib. + _roomCreationLabel.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0]; + _createRoomBtn.enabled = NO; + _createRoomBtn.alpha = 0.5; + + // Init + publicRooms = nil; + highlightedPublicRooms = @[@"#matrix:matrix.org"]; // Add here a room name to highlight its display in public room list +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)dealloc{ + publicRooms = nil; + highlightedPublicRooms = nil; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // Ensure to display room creation section + [self.tableView scrollRectToVisible:_roomCreationLabel.frame animated:NO]; + + if ([[MatrixHandler sharedHandler] isLogged]) { + // Update alias placeholder + _roomAliasTextField.placeholder = [NSString stringWithFormat:@"(e.g. #foo:%@)", [MatrixHandler sharedHandler].homeServer]; + // Refresh listed public rooms + [self refreshPublicRooms]; + } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil]; +} + +#pragma mark - Internals + +- (void)refreshPublicRooms { + // Retrieve public rooms + [[MatrixHandler sharedHandler].mxRestClient publicRooms:^(NSArray *rooms){ + publicRooms = [rooms sortedArrayUsingComparator:^NSComparisonResult(id a, id b) { + + MXPublicRoom *firstRoom = (MXPublicRoom*)a; + MXPublicRoom *secondRoom = (MXPublicRoom*)b; + + return [firstRoom.displayname compare:secondRoom.displayname options:NSCaseInsensitiveSearch]; + }]; + [_publicRoomsTable reloadData]; + } + failure:^(NSError *error){ + NSLog(@"GET public rooms failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + +} + +- (void)dismissKeyboard { + // Hide the keyboard + [_roomNameTextField resignFirstResponder]; + [_roomAliasTextField resignFirstResponder]; + [_participantsTextField resignFirstResponder]; +} + +- (NSString*)alias { + // Extract alias name from alias text field + NSString *alias = _roomAliasTextField.text; + if (alias.length > 1) { + // 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:@""]; + } + + if (! alias.length) { + alias = nil; + } + + return alias; +} + +- (NSArray*)participantsList { + NSMutableArray *participants = [NSMutableArray array]; + + if (_participantsTextField.text.length) { + NSArray *components = [_participantsTextField.text componentsSeparatedByString:@";"]; + + for (NSString *component in components) { + // Remove white space from both ends + NSString *user = [component stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if (user.length > 1 && [user hasPrefix:@"@"]) { + [participants addObject:user]; + } + } + } + + if (participants.count == 0) { + participants = nil; + } + + return participants; +} + +#pragma mark - UITextField delegate + +- (void)onTextFieldChange:(NSNotification *)notif { + NSString *roomName = _roomNameTextField.text; + NSString *roomAlias = _roomAliasTextField.text; + NSString *participants = _participantsTextField.text; + + if (roomName.length || roomAlias.length || participants.length) { + _createRoomBtn.enabled = YES; + _createRoomBtn.alpha = 1; + } else { + _createRoomBtn.enabled = NO; + _createRoomBtn.alpha = 0.5; + } +} + +- (void)textFieldDidBeginEditing:(UITextField *)textField { + if (textField == _roomAliasTextField) { + textField.text = self.alias; + textField.placeholder = @"foo"; + } else if (textField == _participantsTextField) { + if (textField.text.length == 0) { + textField.text = @"@"; + } + } +} + +- (void)textFieldDidEndEditing:(UITextField *)textField { + if (textField == _roomAliasTextField) { + // Compute the new phone number with this string change + NSString * alias = textField.text; + if (alias.length) { + // add homeserver as suffix + textField.text = [NSString stringWithFormat:@"#%@:%@", alias, [MatrixHandler sharedHandler].homeServer]; + } + + textField.placeholder = [NSString stringWithFormat:@"(e.g. #foo:%@)", [MatrixHandler sharedHandler].homeServer]; + } else if (textField == _participantsTextField) { + NSArray *participants = self.participantsList; + textField.text = [participants componentsJoinedByString:@"; "]; + } +} + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + // Auto complete participant IDs + 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:@" @"]; + } else if ([string isEqualToString:@":"]) { + // Add homeserver + participants = [participants stringByAppendingString:[MatrixHandler sharedHandler].homeServer]; + } + + textField.text = participants; + + // Update Create button status + [self onTextFieldChange:nil]; + return NO; + } + } + return YES; +} + +- (BOOL)textFieldShouldReturn:(UITextField*) textField { + // "Done" key has been pressed + [textField resignFirstResponder]; + return YES; +} + +#pragma mark - Actions + +- (IBAction)onButtonPressed:(id)sender { + [self dismissKeyboard]; + + if (sender == _createRoomBtn) { + // Disable button to prevent multiple request + _createRoomBtn.enabled = NO; + + NSString *roomName = _roomNameTextField.text; + if (! roomName.length) { + roomName = nil; + } + + // Create new room + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient createRoom:roomName + visibility:(_roomVisibilityControl.selectedSegmentIndex == 0) ? kMXRoomVisibilityPublic : kMXRoomVisibilityPrivate + room_alias_name:self.alias + topic:nil + success:^(MXCreateRoomResponse *response) { + // Check whether some users must be invited + NSArray *invitedUsers = self.participantsList; + for (NSString *userId in invitedUsers) { + [mxHandler.mxRestClient inviteUser:userId toRoom:response.roomId success:^{ + NSLog(@"%@ has been invited (roomId: %@)", userId, response.roomId); + } failure:^(NSError *error) { + NSLog(@"%@ invitation failed (roomId: %@): %@", userId, response.roomId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + + // Reset text fields + _roomNameTextField.text = nil; + _roomAliasTextField.text = nil; + _participantsTextField.text = nil; + // Open created room + [[AppDelegate theDelegate].masterTabBarController showRoom:response.roomId]; + } failure:^(NSError *error) { + _createRoomBtn.enabled = YES; + NSLog(@"Create room (%@ %@ (%@)) failed: %@", _roomNameTextField.text, self.alias, (_roomVisibilityControl.selectedSegmentIndex == 0) ? @"Public":@"Private", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return publicRooms.count; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + UILabel *sectionHeader = [[UILabel alloc] initWithFrame:[tableView rectForHeaderInSection:section]]; + sectionHeader.font = [UIFont boldSystemFontOfSize:16]; + sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0]; + + if (publicRooms) { + NSString *homeserver = [MatrixHandler sharedHandler].homeServerURL; + if (homeserver.length) { + sectionHeader.text = [NSString stringWithFormat:@" Public Rooms (at %@):", homeserver]; + } else { + sectionHeader.text = @" Public Rooms:"; + } + } else { + sectionHeader.text = @" No Public Rooms"; + } + + return sectionHeader; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Cell is larger for public room with topic + MXPublicRoom *publicRoom = [publicRooms objectAtIndex:indexPath.row]; + if (publicRoom.topic) { + return 60; + } + return 44; +} + +- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + MXPublicRoom *publicRoom = [publicRooms objectAtIndex:indexPath.row]; + UITableViewCell *cell; + + // Check whether this public room has topic + if (publicRoom.topic) { + cell = [_publicRoomsTable dequeueReusableCellWithIdentifier:@"PublicRoomCellSubtitle" forIndexPath:indexPath]; + cell.detailTextLabel.text = publicRoom.topic; + } else { + cell = [_publicRoomsTable dequeueReusableCellWithIdentifier:@"PublicRoomCellBasic" forIndexPath:indexPath]; + } + + // Set room display name + cell.textLabel.text = [publicRoom displayname]; + + // Highlight? + if (cell.textLabel.text && [highlightedPublicRooms indexOfObject:cell.textLabel.text] != NSNotFound) { + cell.textLabel.font = [UIFont boldSystemFontOfSize:20]; + cell.detailTextLabel.font = [UIFont boldSystemFontOfSize:17]; + } else { + cell.textLabel.font = [UIFont systemFontOfSize:19]; + cell.detailTextLabel.font = [UIFont systemFontOfSize:16]; + } + + return cell; +} + +#pragma mark - Table view delegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + + // Check whether the user has already joined the selected public room + MXPublicRoom *publicRoom = [publicRooms objectAtIndex:indexPath.row]; + if ([mxHandler.mxSession room:publicRoom.roomId]) { + // Open selected room + [[AppDelegate theDelegate].masterTabBarController showRoom:publicRoom.roomId]; + } else { + // Join the selected room + UIActivityIndicatorView *loadingWheel = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath]; + if (selectedCell) { + CGPoint center = CGPointMake(selectedCell.frame.size.width / 2, selectedCell.frame.size.height / 2); + loadingWheel.center = center; + [selectedCell addSubview:loadingWheel]; + } + [loadingWheel startAnimating]; + [mxHandler.mxSession joinRoom:publicRoom.roomId success:^(MXRoom *room) { + // Show joined room + [loadingWheel stopAnimating]; + [loadingWheel removeFromSuperview]; + [[AppDelegate theDelegate].masterTabBarController showRoom:publicRoom.roomId]; + } failure:^(NSError *error) { + NSLog(@"Failed to join public room (%@) failed: %@", publicRoom.displayname, error); + //Alert user + [loadingWheel stopAnimating]; + [loadingWheel removeFromSuperview]; + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +@end diff --git a/matrixConsole/ViewController/LoginViewController.h b/matrixConsole/ViewController/LoginViewController.h new file mode 100644 index 000000000..cf5b84442 --- /dev/null +++ b/matrixConsole/ViewController/LoginViewController.h @@ -0,0 +1,22 @@ +/* + 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 + +@interface LoginViewController : UIViewController + +@end + diff --git a/matrixConsole/ViewController/LoginViewController.m b/matrixConsole/ViewController/LoginViewController.m new file mode 100644 index 000000000..0c66336d1 --- /dev/null +++ b/matrixConsole/ViewController/LoginViewController.m @@ -0,0 +1,226 @@ +/* + 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 "LoginViewController.h" + +#import "MatrixHandler.h" +#import "AppDelegate.h" +#import "CustomAlert.h" + +NSString* const defaultHomeserver = @"http://matrix.org"; + +@interface LoginViewController () +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *contentViewBottomConstraint; + +@property (strong, nonatomic) IBOutlet UIScrollView *scrollView; +@property (weak, nonatomic) IBOutlet UIView *contentView; + +@property (weak, nonatomic) IBOutlet UITextField *homeServerTextField; +@property (weak, nonatomic) IBOutlet UITextField *userLoginTextField; +@property (weak, nonatomic) IBOutlet UITextField *passWordTextField; + +@property (weak, nonatomic) IBOutlet UIButton *loginBtn; +@property (weak, nonatomic) IBOutlet UIButton *createAccountBtn; + +@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; + +@end + +@implementation LoginViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + + // Finalize scrollView content size + _contentViewBottomConstraint.constant = 0; + + // Force contentView in full width + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.contentView + attribute:NSLayoutAttributeLeading + relatedBy:0 + toItem:self.view + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:0]; + [self.view addConstraint:leftConstraint]; + + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.contentView + attribute:NSLayoutAttributeTrailing + relatedBy:0 + toItem:self.view + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:0]; + [self.view addConstraint:rightConstraint]; + + // Prefill text field + _userLoginTextField.text = [[MatrixHandler sharedHandler] userLogin]; + _homeServerTextField.text = [[MatrixHandler sharedHandler] homeServerURL]; + if (! _homeServerTextField.text.length) { + [[MatrixHandler sharedHandler] setHomeServerURL:defaultHomeserver]; + self.homeServerTextField.text = defaultHomeserver; + } + _passWordTextField.text = nil; + _loginBtn.enabled = NO; + _loginBtn.alpha = 0.5; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil]; +} + +- (void)onKeyboardWillShow:(NSNotification *)notif { + NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey]; + CGRect endRect = rectVal.CGRectValue; + + UIEdgeInsets insets = self.scrollView.contentInset; + // Handle portrait/landscape mode + insets.bottom = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height; + self.scrollView.contentInset = insets; + + for (UITextField *tf in @[ self.userLoginTextField, self.passWordTextField, self.homeServerTextField]) { + if ([tf isFirstResponder]) { + CGRect tfFrame = tf.frame; + [self.scrollView scrollRectToVisible:tfFrame animated:YES]; + } + } +} + +- (void)onKeyboardWillHide:(NSNotification *)notif { + UIEdgeInsets insets = self.scrollView.contentInset; + insets.bottom = 0; + self.scrollView.contentInset = insets; +} + +- (void)dismissKeyboard +{ + // Hide the keyboard + [_userLoginTextField resignFirstResponder]; + [_passWordTextField resignFirstResponder]; + [_homeServerTextField resignFirstResponder]; +} + +#pragma mark - UITextField delegate + +- (void)onTextFieldChange:(NSNotification *)notif { + NSString *user = _userLoginTextField.text; + NSString *pass = _passWordTextField.text; + NSString *homeServerURL = _homeServerTextField.text; + + if (user.length && pass.length && homeServerURL.length) { + _loginBtn.enabled = YES; + _loginBtn.alpha = 1; + } else { + _loginBtn.enabled = NO; + _loginBtn.alpha = 0.5; + } +} + +- (void)textFieldDidEndEditing:(UITextField *)textField +{ + MatrixHandler *matrix = [MatrixHandler sharedHandler]; + + if (textField == _userLoginTextField) { + [matrix setUserLogin:textField.text]; + } + else if (textField == _homeServerTextField) { + [matrix setHomeServerURL:textField.text]; + } +} + +- (BOOL)textFieldShouldReturn:(UITextField*) textField +{ + if (textField == _userLoginTextField) { + // "Next" key has been pressed + [_passWordTextField becomeFirstResponder]; + } + else { + // "Done" key has been pressed + [textField resignFirstResponder]; + + if (_loginBtn.isEnabled) + { + // Launch authentication now + [self onButtonPressed:_loginBtn]; + } + } + + return YES; +} + +#pragma mark - + +- (IBAction)onButtonPressed:(id)sender +{ + [self dismissKeyboard]; + + if (sender == _loginBtn) { + MatrixHandler *matrix = [MatrixHandler sharedHandler]; + + if (matrix.mxRestClient) + { + // Disable login button to prevent multiple requests + _loginBtn.enabled = NO; + [_activityIndicator startAnimating]; + + [matrix.mxRestClient loginWithUser:matrix.userLogin andPassword:_passWordTextField.text + success:^(MXCredentials *credentials){ + [_activityIndicator stopAnimating]; + + // Report credentials + [matrix setUserId:credentials.userId]; + [matrix setAccessToken:credentials.accessToken]; + [matrix setHomeServer:credentials.homeServer]; + + [self dismissViewControllerAnimated:YES completion:nil]; + } + failure:^(NSError *error){ + [_activityIndicator stopAnimating]; + _loginBtn.enabled = YES; + + NSLog(@"Login failed: %@", error); + //Alert user + CustomAlert *alert = [[CustomAlert alloc] initWithTitle:@"Login Failed" message:@"Invalid username/password" style:CustomAlertStyleAlert]; + [alert addActionWithTitle:@"Dismiss" style:CustomAlertActionStyleCancel handler:^(CustomAlert *alert) {}]; + [alert showInViewController:self]; + }]; + } + } else if (sender == _createAccountBtn){ + // TODO + } +} + +@end diff --git a/matrixConsole/ViewController/MasterTabBarController.h b/matrixConsole/ViewController/MasterTabBarController.h new file mode 100644 index 000000000..120b55663 --- /dev/null +++ b/matrixConsole/ViewController/MasterTabBarController.h @@ -0,0 +1,36 @@ +/* + 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 + +#define TABBAR_HOME_INDEX 0 +#define TABBAR_RECENTS_INDEX 1 +#define TABBAR_SETTINGS_INDEX 2 +#define TABBAR_COUNT 3 + +@interface MasterTabBarController : UITabBarController + +- (void)showLoginScreen; +- (void)showRoomCreationForm; +- (void)showRoom:(NSString*)roomId; + +- (void)presentMediaPicker:(UIImagePickerController*)mediaPicker; +- (void)dismissMediaPicker; + +@property (strong, nonatomic) NSString *visibleRoomId; // nil if no room is presently visible + +@end + diff --git a/matrixConsole/ViewController/MasterTabBarController.m b/matrixConsole/ViewController/MasterTabBarController.m new file mode 100644 index 000000000..aa9a92c81 --- /dev/null +++ b/matrixConsole/ViewController/MasterTabBarController.m @@ -0,0 +1,137 @@ +/* + 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 "MasterTabBarController.h" +#import "MatrixHandler.h" + +#import "RecentsViewController.h" + +@interface MasterTabBarController () { + UINavigationController *recentsNavigationController; + RecentsViewController *recentsViewController; + + UIImagePickerController *mediaPicker; +} + +@end + +@implementation MasterTabBarController + +@synthesize visibleRoomId; + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + + // To simplify navigation into the app, we retrieve here the navigation controller and the view controller related + // to the recents list in Recents Tab. + // Note: UISplitViewController is not supported on iPhone for iOS < 8.0 + UIViewController* recents = [self.viewControllers objectAtIndex:TABBAR_RECENTS_INDEX]; + recentsNavigationController = nil; + if ([recents isKindOfClass:[UISplitViewController class]]) { + UISplitViewController *splitViewController = (UISplitViewController *)recents; + recentsNavigationController = [splitViewController.viewControllers objectAtIndex:0]; + } else if ([recents isKindOfClass:[UINavigationController class]]) { + recentsNavigationController = (UINavigationController*)recents; + } + + if (recentsNavigationController) { + for (UIViewController *viewController in recentsNavigationController.viewControllers) { + if ([viewController isKindOfClass:[RecentsViewController class]]) { + recentsViewController = (RecentsViewController*)viewController; + } + } + } +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + if (! [[MatrixHandler sharedHandler] isLogged]) { + [self showLoginScreen]; + } +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)dealloc { + recentsNavigationController = nil; + recentsViewController = nil; + + [self dismissMediaPicker]; +} + +#pragma mark - + +- (void)restoreInitialDisplay { + // Dismiss potential media picker + if (mediaPicker) { + if (mediaPicker.delegate && [mediaPicker.delegate respondsToSelector:@selector(imagePickerControllerDidCancel:)]) { + [mediaPicker.delegate imagePickerControllerDidCancel:mediaPicker]; + } else { + [self dismissMediaPicker]; + } + } + + // Force back to recents list if room details is displayed in Recents Tab + if (recentsViewController) { + [recentsNavigationController popToViewController:recentsViewController animated:NO]; + } +} + +#pragma mark - + +- (void)showLoginScreen { + [self restoreInitialDisplay]; + + [self performSegueWithIdentifier:@"showLogin" sender:self]; +} + +- (void)showRoomCreationForm { + // Switch in Home Tab + [self setSelectedIndex:TABBAR_HOME_INDEX]; +} + +- (void)showRoom:(NSString*)roomId { + [self restoreInitialDisplay]; + + // Switch on Recents Tab + [self setSelectedIndex:TABBAR_RECENTS_INDEX]; + + // Select room to display its details (dispatch this action in order to let TabBarController end its refresh) + dispatch_async(dispatch_get_main_queue(), ^{ + recentsViewController.preSelectedRoomId = roomId; + }); +} + +- (void)presentMediaPicker:(UIImagePickerController*)aMediaPicker { + [self dismissMediaPicker]; + [self presentViewController:aMediaPicker animated:YES completion:^{ + mediaPicker = aMediaPicker; + }]; +} +- (void)dismissMediaPicker { + if (mediaPicker) { + [self dismissViewControllerAnimated:NO completion:nil]; + mediaPicker.delegate = nil; + mediaPicker = nil; + } +} + +@end diff --git a/matrixConsole/ViewController/RecentsViewController.h b/matrixConsole/ViewController/RecentsViewController.h new file mode 100644 index 000000000..aae4d01ec --- /dev/null +++ b/matrixConsole/ViewController/RecentsViewController.h @@ -0,0 +1,26 @@ +/* + 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 + +@class RoomViewController; + +@interface RecentsViewController : UITableViewController + +@property (strong, nonatomic) NSString *preSelectedRoomId; // set a non-nil value to this property will open room details + +@end + diff --git a/matrixConsole/ViewController/RecentsViewController.m b/matrixConsole/ViewController/RecentsViewController.m new file mode 100644 index 000000000..8e4b4e665 --- /dev/null +++ b/matrixConsole/ViewController/RecentsViewController.m @@ -0,0 +1,311 @@ +/* + 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 "RecentsViewController.h" +#import "RoomViewController.h" + +#import "RecentsTableViewCell.h" + +#import "AppDelegate.h" +#import "MatrixHandler.h" + +@interface RecentsViewController () { + NSMutableArray *recents; + id recentsListener; + + // Date formatter + NSDateFormatter *dateFormatter; + + RoomViewController *currentRoomViewController; +} +@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; + +@end + +@implementation RecentsViewController + +- (void)awakeFromNib { + [super awakeFromNib]; + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + self.clearsSelectionOnViewWillAppear = NO; + self.preferredContentSize = CGSizeMake(320.0, 600.0); + } +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + self.navigationItem.leftBarButtonItem = self.editButtonItem; + + UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(createNewRoom:)]; + self.navigationItem.rightBarButtonItem = addButton; + + // Add activity indicator + [self.view addSubview:_activityIndicator]; + _activityIndicator.center = CGPointMake(self.view.center.x, 100); + [self.view bringSubviewToFront:_activityIndicator]; + + // Initialisation + recents = nil; + + NSString *dateFormat = @"MMM dd HH:mm"; + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]]]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [dateFormatter setDateFormat:dateFormat]; +} + +- (void)dealloc { + if (currentRoomViewController) { + currentRoomViewController.roomId = nil; + } + if (recentsListener) { + [[MatrixHandler sharedHandler].mxSession removeListener:recentsListener]; + recentsListener = nil; + } + recents = nil; + _preSelectedRoomId = nil; + + if (dateFormatter) { + dateFormatter = nil; + } +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // Refresh recents table + [self configureView]; + [[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"isInitialSyncDone" options:0 context:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + // Leave potential editing mode + [self setEditing:NO]; + + if (recentsListener) { + [[MatrixHandler sharedHandler].mxSession removeListener:recentsListener]; + recentsListener = nil; + } + + _preSelectedRoomId = nil; + [[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"isInitialSyncDone"]; +} + +#pragma mark - + +- (void)setPreSelectedRoomId:(NSString *)roomId { + _preSelectedRoomId = nil; + + if (roomId) { + // Check whether recents update is in progress + if ([_activityIndicator isAnimating]) { + // Postpone room details display + _preSelectedRoomId = roomId; + return; + } + + // Look for the room index in recents list + NSIndexPath *indexPath = nil; + for (NSUInteger index = 0; index < recents.count; index++) { + MXEvent *mxEvent = [recents objectAtIndex:index]; + if ([roomId isEqualToString:mxEvent.roomId]) { + indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + break; + } + } + + if (indexPath) { + // Open details view + [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle]; + UITableViewCell *recentCell = [self.tableView cellForRowAtIndexPath:indexPath]; + [self performSegueWithIdentifier:@"showDetail" sender:recentCell]; + } else { + NSLog(@"We are not able to open room (%@) because it does not appear in recents yet", roomId); + // Postpone room details display. We run activity indicator until recents are updated + _preSelectedRoomId = roomId; + // Start activity indicator + [_activityIndicator startAnimating]; + } + } +} + +#pragma mark - Internal methods + +- (void)configureView { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + + // Remove potential listener + if (recentsListener && mxHandler.mxSession) { + [mxHandler.mxSession removeListener:recentsListener]; + recentsListener = nil; + } + + [_activityIndicator startAnimating]; + + if ([mxHandler isInitialSyncDone] || [mxHandler isLogged] == NO) { + // Update recents + if (mxHandler.mxSession) { + recents = [NSMutableArray arrayWithArray:mxHandler.mxSession.recents]; + // Register recent listener + recentsListener = [mxHandler.mxSession listenToEventsOfTypes:mxHandler.mxSession.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, id customObject) { + // consider only live event + if (direction == MXEventDirectionForwards) { + // Refresh the whole recents list + recents = [NSMutableArray arrayWithArray:mxHandler.mxSession.recents]; + // Reload table + [self.tableView reloadData]; + [_activityIndicator stopAnimating]; + + // Check whether a room is preselected + if (_preSelectedRoomId) { + self.preSelectedRoomId = _preSelectedRoomId; + } + } + }]; + } else { + recents = nil; + } + + // Reload table + [self.tableView reloadData]; + [_activityIndicator stopAnimating]; + + // Check whether a room is preselected + if (_preSelectedRoomId) { + self.preSelectedRoomId = _preSelectedRoomId; + } + } +} + +- (void)createNewRoom:(id)sender { + [[AppDelegate theDelegate].masterTabBarController showRoomCreationForm]; +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([@"isInitialSyncDone" isEqualToString:keyPath]) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self configureView]; + }); + } +} + +#pragma mark - Segues + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + if ([[segue identifier] isEqualToString:@"showDetail"]) { + NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; + MXEvent *mxEvent = recents[indexPath.row]; + + UIViewController *controller; + if ([[segue destinationViewController] isKindOfClass:[UINavigationController class]]) { + controller = [[segue destinationViewController] topViewController]; + } else { + controller = [segue destinationViewController]; + } + + if ([controller isKindOfClass:[RoomViewController class]]) { + if (currentRoomViewController) { + if ((currentRoomViewController != controller) || (![currentRoomViewController.roomId isEqualToString:mxEvent.roomId])) { + // Release the current one + currentRoomViewController.roomId = nil; + } + } + currentRoomViewController = (RoomViewController *)controller; + currentRoomViewController.roomId = mxEvent.roomId; + } + + controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; + controller.navigationItem.leftItemsSupplementBackButton = YES; + } +} + +#pragma mark - Table View + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return recents.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 70; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + RecentsTableViewCell *cell = (RecentsTableViewCell*)[tableView dequeueReusableCellWithIdentifier:@"RecentsCell" forIndexPath:indexPath]; + + MXEvent *mxEvent = recents[indexPath.row]; + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + MXRoom *mxRoom = [mxHandler.mxSession room:mxEvent.roomId]; + + cell.roomTitle.text = [mxRoom.state displayname]; + cell.lastEventDescription.text = [mxHandler displayTextFor:mxEvent inSubtitleMode:YES]; + + // Set in bold public room name + if (mxRoom.state.isPublic) { + cell.roomTitle.font = [UIFont boldSystemFontOfSize:20]; + } else { + cell.roomTitle.font = [UIFont systemFontOfSize:19]; + } + + if (mxEvent.originServerTs != kMXUndefinedTimestamp) { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:mxEvent.originServerTs/1000]; + cell.recentDate.text = [dateFormatter stringFromDate:date]; + } else { + cell.recentDate.text = nil; + } + return cell; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + // Return NO if you do not want the specified item to be editable. + return YES; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Leave the selected room + MXEvent *mxEvent = recents[indexPath.row]; + MXRoom *mxRoom = [[MatrixHandler sharedHandler].mxSession room:mxEvent.roomId]; + [mxRoom leave:^{ + // Refresh table display + [recents removeObjectAtIndex:indexPath.row]; + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } failure:^(NSError *error) { + NSLog(@"Failed to leave room (%@) failed: %@", mxEvent.roomId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } +} + +@end diff --git a/matrixConsole/ViewController/RoomViewController.h b/matrixConsole/ViewController/RoomViewController.h new file mode 100644 index 000000000..b3959f5f0 --- /dev/null +++ b/matrixConsole/ViewController/RoomViewController.h @@ -0,0 +1,24 @@ +/* + 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 + +@interface RoomViewController : UIViewController + +@property (strong, nonatomic) NSString *roomId; + +@end + diff --git a/matrixConsole/ViewController/RoomViewController.m b/matrixConsole/ViewController/RoomViewController.m new file mode 100644 index 000000000..b57ab3029 --- /dev/null +++ b/matrixConsole/ViewController/RoomViewController.m @@ -0,0 +1,1834 @@ +/* + 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 + +#import + +#import "RoomViewController.h" +#import "RoomMessageTableCell.h" +#import "RoomMemberTableCell.h" + +#import "MatrixHandler.h" +#import "AppDelegate.h" +#import "AppSettings.h" + +#import "MediaManager.h" + +#define UPLOAD_FILE_SIZE 5000000 + +#define ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH 200 + +#define ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_DEFAULT 10 +#define ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_IN_CHUNK (-5) +#define ROOM_MESSAGE_CELL_TEXTVIEW_EDGE_INSET_TOP_IN_CHUNK ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_IN_CHUNK +#define ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_DEFAULT 0 +#define ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_GROUPED_CELL (-5) + +#define ROOM_MESSAGE_CELL_IMAGE_MARGIN 8 + +NSString *const kCmdChangeDisplayName = @"/nick"; +NSString *const kCmdEmote = @"/me"; +NSString *const kCmdJoinRoom = @"/join"; +NSString *const kCmdKickUser = @"/kick"; +NSString *const kCmdBanUser = @"/ban"; +NSString *const kCmdUnbanUser = @"/unban"; +NSString *const kCmdSetUserPowerLevel = @"/op"; +NSString *const kCmdResetUserPowerLevel = @"/deop"; + +NSString *const kLocalEchoEventIdPrefix = @"localEcho-"; +NSString *const kFailedEventId = @"failedEventId"; + + +@interface RoomViewController () { + BOOL forceScrollToBottomOnViewDidAppear; + BOOL isJoinRequestInProgress; + + MXRoom *mxRoom; + + // Messages + NSMutableArray *messages; + id messagesListener; + NSString *mostRecentEventIdOnViewWillDisappear; + + // Back pagination + BOOL isBackPaginationInProgress; + NSUInteger backPaginationAddedItemsNb; + + // Members list + NSArray *members; + id membersListener; + + // Attachment handling + CustomImageView *highResImage; + NSString *AVAudioSessionCategory; + MPMoviePlayerController *videoPlayer; + + // Date formatter (nil if dateTimeLabel is hidden) + NSDateFormatter *dateFormatter; + + // Text view settings + NSAttributedString *initialAttributedStringForOutgoingMessage; + NSAttributedString *initialAttributedStringForIncomingMessage; + + // Cache + NSMutableArray *tmpCachedAttachments; +} + +@property (weak, nonatomic) IBOutlet UINavigationItem *roomNavItem; +@property (weak, nonatomic) IBOutlet UITextField *roomNameTextField; +@property (weak, nonatomic) IBOutlet UITableView *messagesTableView; +@property (weak, nonatomic) IBOutlet UIView *controlView; +@property (weak, nonatomic) IBOutlet UIButton *optionBtn; +@property (weak, nonatomic) IBOutlet UITextField *messageTextField; +@property (weak, nonatomic) IBOutlet UIButton *sendBtn; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *controlViewBottomConstraint; +@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; +@property (weak, nonatomic) IBOutlet UIView *membersView; +@property (weak, nonatomic) IBOutlet UITableView *membersTableView; + +@property (strong, nonatomic) CustomAlert *actionMenu; +@end + +@implementation RoomViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + forceScrollToBottomOnViewDidAppear = YES; + mostRecentEventIdOnViewWillDisappear = nil; + + UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight]; + [button addTarget:self action:@selector(showHideRoomMembers:) forControlEvents:UIControlEventTouchUpInside]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; + + // Add tap detection on members view in order to hide members when the user taps outside members list + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideRoomMembers)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [tap setDelegate:self]; + [self.membersView addGestureRecognizer:tap]; + + _sendBtn.enabled = NO; + _sendBtn.alpha = 0.5; +} + +- (void)dealloc { + // Clear temporary cached attachments (used for local echo) + NSUInteger index = tmpCachedAttachments.count; + NSError *error = nil; + while (index--) { + if (![[NSFileManager defaultManager] removeItemAtPath:[tmpCachedAttachments objectAtIndex:index] error:&error]) { + NSLog(@"Fail to delete cached media: %@", error); + } + } + tmpCachedAttachments = nil; + + [self hideAttachmentView]; + + messages = nil; + if (messagesListener) { + [mxRoom removeListener:messagesListener]; + messagesListener = nil; + } + mxRoom = nil; + + members = nil; + if (membersListener) { + membersListener = nil; + } + + if (self.actionMenu) { + [self.actionMenu dismiss:NO]; + self.actionMenu = nil; + } + + if (dateFormatter) { + dateFormatter = nil; + } + + initialAttributedStringForOutgoingMessage = nil; + initialAttributedStringForIncomingMessage = nil; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + if (isBackPaginationInProgress || isJoinRequestInProgress) { + // Busy - be sure that activity indicator is running + [_activityIndicator startAnimating]; + } + + if (mostRecentEventIdOnViewWillDisappear) { + if (messages) { + MXEvent *mxEvent = [messages lastObject]; + if ([mxEvent.eventId isEqualToString:mostRecentEventIdOnViewWillDisappear] == NO) { + // Some new events have been received for this room, scroll to bottom to focus on them + forceScrollToBottomOnViewDidAppear = YES; + } + } + mostRecentEventIdOnViewWillDisappear = nil; + } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextFieldChange:) name:UITextFieldTextDidChangeNotification object:nil]; + + // Set visible room id + [AppDelegate theDelegate].masterTabBarController.visibleRoomId = self.roomId; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + // hide action + if (self.actionMenu) { + [self.actionMenu dismiss:NO]; + self.actionMenu = nil; + } + + // Hide members by default + [self hideRoomMembers]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidChangeNotification object:nil]; + + // We store the eventID of the last known event (if any) in order to scroll to bottom when view will appear, if new events have been received + if (messages) { + MXEvent *mxEvent = [messages lastObject]; + mostRecentEventIdOnViewWillDisappear = mxEvent.eventId; + } + + // Reset visible room id + [AppDelegate theDelegate].masterTabBarController.visibleRoomId = nil; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + if (forceScrollToBottomOnViewDidAppear) { + // Scroll to the bottom + [self scrollToBottomAnimated:animated]; + forceScrollToBottomOnViewDidAppear = NO; + } +} + +#pragma mark - room ID + +- (void)setRoomId:(NSString *)roomId { + if ([self.roomId isEqualToString:roomId] == NO) { + _roomId = roomId; + // Reload room data here + [self configureView]; + } +} + +#pragma mark - UIGestureRecognizer delegate + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.view == self.membersView) { + // Compute actual frame of the displayed members list + CGRect frame = self.membersTableView.frame; + if (self.membersTableView.tableFooterView.frame.origin.y < frame.size.height) { + frame.size.height = self.membersTableView.tableFooterView.frame.origin.y; + } + // gestureRecognizer should begin only if tap is outside members list + return !CGRectContainsPoint(frame, [gestureRecognizer locationInView:self.membersView]); + } + return YES; +} + +#pragma mark - Internal methods + +- (void)configureView { + // Check whether a request is in progress to join the room + if (isJoinRequestInProgress) { + // Busy - be sure that activity indicator is running + [_activityIndicator startAnimating]; + return; + } + + // Remove potential listener + if (messagesListener && mxRoom) { + [mxRoom removeListener:messagesListener]; + messagesListener = nil; + } + // The whole room history is flushed here to rebuild it from the current instant (live) + messages = nil; + // Disable room title edition + self.roomNameTextField.enabled = NO; + + // Update room data + if (self.roomId) { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + mxRoom = [mxHandler.mxSession room:self.roomId]; + + // Update room title + self.roomNameTextField.text = mxRoom.state.displayname; + + // Check first whether we have to join the room + if (mxRoom.state.membership == MXMembershipInvite) { + isJoinRequestInProgress = YES; + [_activityIndicator startAnimating]; + [mxRoom join:^{ + [_activityIndicator stopAnimating]; + isJoinRequestInProgress = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + [self configureView]; + }); + } failure:^(NSError *error) { + [_activityIndicator stopAnimating]; + isJoinRequestInProgress = NO; + NSLog(@"Failed to join room (%@): %@", mxRoom.state.displayname, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + return; + } + + // Enable room title edition + self.roomNameTextField.enabled = YES; + + messages = [NSMutableArray array]; + // Register a listener to handle messages + messagesListener = [mxRoom listenToEventsOfTypes:mxHandler.mxSession.eventsFilterForMessages onEvent:^(MXEvent *event, MXEventDirection direction, MXRoomState *roomState) { + BOOL shouldScrollToBottom = NO; + + // Handle first live events + if (direction == MXEventDirectionForwards) { + // For outgoing message, remove the temporary event + if ([event.userId isEqualToString:[MatrixHandler sharedHandler].userId]) { + NSUInteger index = messages.count; + while (index--) { + MXEvent *mxEvent = [messages objectAtIndex:index]; + if ([mxEvent.eventId isEqualToString:event.eventId]) { + [messages replaceObjectAtIndex:index withObject:event]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + return; + } + } + } + // Here a new event is added + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0]; + [messages addObject:event]; + shouldScrollToBottom = (self.messagesTableView.contentOffset.y + self.messagesTableView.frame.size.height >= self.messagesTableView.contentSize.height); + + // Refresh table display (Disable animation during cells insertion to prevent flickering) + [UIView setAnimationsEnabled:NO]; + [self.messagesTableView beginUpdates]; + if (indexPath.row > 0) { + NSIndexPath *prevIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[prevIndexPath] withRowAnimation:UITableViewRowAnimationNone]; + } + [self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + [self.messagesTableView endUpdates]; + [UIView setAnimationsEnabled:YES]; + } else if (isBackPaginationInProgress && direction == MXEventDirectionBackwards) { + // Back pagination is in progress, we add an old event at the beginning of messages + [messages insertObject:event atIndex:0]; + backPaginationAddedItemsNb++; + // Display is refreshed at the end of back pagination (see onComplete block) + } + + if (shouldScrollToBottom) { + [self scrollToBottomAnimated:YES]; + } + }]; + + // Trigger a back pagination by reseting first backState to get room history from live + [mxRoom resetBackState]; + [self triggerBackPagination]; + } else { + mxRoom = nil; + // Update room title + self.roomNameTextField.text = nil; + } + + [self.messagesTableView reloadData]; +} + +- (void)scrollToBottomAnimated:(BOOL)animated { + // Scroll table view to the bottom + NSInteger rowNb = messages.count; + if (rowNb) { + [self.messagesTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(rowNb - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated]; + } +} + +- (void)triggerBackPagination { + // Check whether a back pagination is already in progress + if (isBackPaginationInProgress) { + return; + } + + if (mxRoom.canPaginate) { + [_activityIndicator startAnimating]; + isBackPaginationInProgress = YES; + backPaginationAddedItemsNb = 0; + + [mxRoom paginateBackMessages:20 complete:^{ + if (backPaginationAddedItemsNb) { + // Prepare insertion of new rows at the top of the table (compute cumulative height of added cells) + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:backPaginationAddedItemsNb]; + NSIndexPath *indexPath; + CGFloat verticalOffset = 0; + for (NSUInteger index = 0; index < backPaginationAddedItemsNb; index++) { + indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [indexPaths addObject:indexPath]; + verticalOffset += [self tableView:self.messagesTableView heightForRowAtIndexPath:indexPath]; + } + + // Disable animation during cells insertion to prevent flickering + [UIView setAnimationsEnabled:NO]; + // Store the current content offset + CGPoint contentOffset = self.messagesTableView.contentOffset; + [self.messagesTableView beginUpdates]; + [self.messagesTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + [self.messagesTableView endUpdates]; + // Enable animation again + [UIView setAnimationsEnabled:YES]; + // Fix vertical offset in order to prevent scrolling down + contentOffset.y += verticalOffset; + [self.messagesTableView setContentOffset:contentOffset animated:NO]; + [_activityIndicator stopAnimating]; + isBackPaginationInProgress = NO; + + // Move the current message at the middle of the visible area (dispatch this action in order to let table end its refresh) + indexPath = [NSIndexPath indexPathForRow:(backPaginationAddedItemsNb - 1) inSection:0]; + backPaginationAddedItemsNb = 0; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.messagesTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; + }); + } else { + // Here there was no event related to the `messages` property + [_activityIndicator stopAnimating]; + isBackPaginationInProgress = NO; + // Trigger a new back pagination (if possible) + [self triggerBackPagination]; + } + } failure:^(NSError *error) { + [_activityIndicator stopAnimating]; + isBackPaginationInProgress = NO; + backPaginationAddedItemsNb = 0; + NSLog(@"Failed to paginate back: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } +} + +# pragma mark - Room members + +- (void)showHideRoomMembers:(id)sender { + // Check whether the members list is displayed + if (members) { + [self hideRoomMembers]; + } else { + [self hideAttachmentView]; + [self showRoomMembers]; + } +} + +- (void)updateRoomMembers { + members = [[mxRoom.state members] sortedArrayUsingComparator:^NSComparisonResult(MXRoomMember *member1, MXRoomMember *member2) { + // Move banned and left members at the end of the list + if (member1.membership == MXMembershipLeave || member1.membership == MXMembershipBan) { + if (member2.membership != MXMembershipLeave && member2.membership != MXMembershipBan) { + return NSOrderedDescending; + } + } else if (member2.membership == MXMembershipLeave || member2.membership == MXMembershipBan) { + return NSOrderedAscending; + } + + // Move invited members just before left and banned members + if (member1.membership == MXMembershipInvite) { + if (member2.membership != MXMembershipInvite) { + return NSOrderedDescending; + } + } else if (member2.membership == MXMembershipInvite) { + return NSOrderedAscending; + } + + if ([[AppSettings sharedSettings] sortMembersUsingLastSeenTime]) { + // Get the users that correspond to these members + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + MXUser *user1 = [mxHandler.mxSession user:member1.userId]; + MXUser *user2 = [mxHandler.mxSession user:member2.userId]; + + // Move users who are not online or unavailable at the end (before invited users) + if ((user1.presence == MXPresenceOnline) || (user1.presence == MXPresenceUnavailable)) { + if ((user2.presence != MXPresenceOnline) && (user2.presence != MXPresenceUnavailable)) { + return NSOrderedAscending; + } + } else if ((user2.presence == MXPresenceOnline) || (user2.presence == MXPresenceUnavailable)) { + return NSOrderedDescending; + } else { + // Here both users are neither online nor unavailable (the lastActive ago is useless) + // We will sort them according to their display, by keeping in front the offline users + if (user1.presence == MXPresenceOffline) { + if (user2.presence != MXPresenceOffline) { + return NSOrderedAscending; + } + } else if (user2.presence == MXPresenceOffline) { + return NSOrderedDescending; + } + return [[mxRoom.state memberName:member1.userId] compare:[mxRoom.state memberName:member2.userId] options:NSCaseInsensitiveSearch]; + } + + // Consider user's lastActive ago value + if (user1.lastActiveAgo < user2.lastActiveAgo) { + return NSOrderedAscending; + } else if (user1.lastActiveAgo == user2.lastActiveAgo) { + return [[mxRoom.state memberName:member1.userId] compare:[mxRoom.state memberName:member2.userId] options:NSCaseInsensitiveSearch]; + } + return NSOrderedDescending; + } else { + // Move user without display name at the end (before invited users) + if (member1.displayname.length) { + if (!member2.displayname.length) { + return NSOrderedAscending; + } + } else if (member2.displayname.length) { + return NSOrderedDescending; + } + + return [[mxRoom.state memberName:member1.userId] compare:[mxRoom.state memberName:member2.userId] options:NSCaseInsensitiveSearch]; + } + }]; +} + +- (void)showRoomMembers { + // Dismiss keyboard + [self dismissKeyboard]; + + [self updateRoomMembers]; + // Register a listener for events that concern room members + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + NSArray *mxMembersEvents = @[ + kMXEventTypeStringRoomMember, + kMXEventTypeStringRoomPowerLevels, + kMXEventTypeStringPresence + ]; + 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:self.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; + } + // Refresh members list + [self updateRoomMembers]; + [self.membersTableView reloadData]; + } + }]; + + self.membersView.hidden = NO; + [self.membersTableView reloadData]; +} + +- (void)hideRoomMembers { + if (membersListener) { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxSession removeListener:membersListener]; + membersListener = nil; + } + self.membersView.hidden = YES; + members = nil; +} + +# pragma mark - Attachment handling + +- (void)showAttachmentView:(UIGestureRecognizer *)gestureRecognizer { + CustomImageView *attachment = (CustomImageView*)gestureRecognizer.view; + [self dismissKeyboard]; + + // Retrieve attachment information + NSDictionary *content = attachment.mediaInfo; + NSString *msgtype = content[@"msgtype"]; + if ([msgtype isEqualToString:kMXMessageTypeImage]) { + NSString *url =content[@"url"]; + if (url.length) { + highResImage = [[CustomImageView alloc] initWithFrame:self.membersView.frame]; + highResImage.contentMode = UIViewContentModeScaleAspectFit; + highResImage.backgroundColor = [UIColor blackColor]; + highResImage.imageURL = url; + [self.view addSubview:highResImage]; + + // Add tap recognizer to hide attachment + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideAttachmentView)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [highResImage addGestureRecognizer:tap]; + highResImage.userInteractionEnabled = YES; + } + } else if ([msgtype isEqualToString:kMXMessageTypeVideo]) { + NSString *url =content[@"url"]; + if (url.length) { + NSString *mimetype = nil; + if (content[@"info"]) { + mimetype = content[@"info"][@"mimetype"]; + } + AVAudioSessionCategory = [[AVAudioSession sharedInstance] category]; + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; + videoPlayer = [[MPMoviePlayerController alloc] init]; + if (videoPlayer != nil) { + videoPlayer.scalingMode = MPMovieScalingModeAspectFit; + [self.view addSubview:videoPlayer.view]; + [videoPlayer setFullscreen:YES animated:NO]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(moviePlayerPlaybackDidFinishNotification:) + name:MPMoviePlayerPlaybackDidFinishNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(moviePlayerWillExitFullscreen:) + name:MPMoviePlayerWillExitFullscreenNotification + object:videoPlayer]; + [MediaManager prepareMedia:url mimeType:mimetype success:^(NSString *cacheFilePath) { + if (cacheFilePath) { + if (tmpCachedAttachments == nil) { + tmpCachedAttachments = [NSMutableArray array]; + } + if ([tmpCachedAttachments indexOfObject:cacheFilePath]) { + [tmpCachedAttachments addObject:cacheFilePath]; + } + } + videoPlayer.contentURL = [NSURL fileURLWithPath:cacheFilePath]; + [videoPlayer play]; + } failure:^(NSError *error) { + [self hideAttachmentView]; + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + } + } else if ([msgtype isEqualToString:kMXMessageTypeAudio]) { + } else if ([msgtype isEqualToString:kMXMessageTypeLocation]) { + } +} + +- (void)hideAttachmentView { + [[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerPlaybackDidFinishNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerWillExitFullscreenNotification object:nil]; + + if (highResImage) { + [highResImage removeFromSuperview]; + highResImage = nil; + } + // Restore audio category + if (AVAudioSessionCategory) { + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategory error:nil]; + } + if (videoPlayer) { + [videoPlayer stop]; + [videoPlayer setFullscreen:NO]; + [videoPlayer.view removeFromSuperview]; + videoPlayer = nil; + } +} + +- (void)moviePlayerWillExitFullscreen:(NSNotification*)notification { + if (notification.object == videoPlayer) { + [self hideAttachmentView]; + } +} + +- (void)moviePlayerPlaybackDidFinishNotification:(NSNotification *)notification { + NSDictionary *notificationUserInfo = [notification userInfo]; + NSNumber *resultValue = [notificationUserInfo objectForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey]; + MPMovieFinishReason reason = [resultValue intValue]; + + // error cases + if (reason == MPMovieFinishReasonPlaybackError) { + NSError *mediaPlayerError = [notificationUserInfo objectForKey:@"error"]; + if (mediaPlayerError) { + NSLog(@"Playback failed with error description: %@", [mediaPlayerError localizedDescription]); + [self hideAttachmentView]; + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:mediaPlayerError]; + } + } +} + +#pragma mark - Keyboard handling + +- (void)onKeyboardWillShow:(NSNotification *)notif { + NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey]; + CGRect endRect = rectVal.CGRectValue; + + UIEdgeInsets insets = self.messagesTableView.contentInset; + // Handle portrait/landscape mode + insets.bottom = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height; + self.messagesTableView.contentInset = insets; + + [self scrollToBottomAnimated:YES]; + + // Move up control view + // Don't forget the offset related to tabBar + _controlViewBottomConstraint.constant = insets.bottom - [AppDelegate theDelegate].masterTabBarController.tabBar.frame.size.height; +} + +- (void)onKeyboardWillHide:(NSNotification *)notif { + UIEdgeInsets insets = self.messagesTableView.contentInset; + insets.bottom = self.controlView.frame.size.height; + self.messagesTableView.contentInset = insets; + + _controlViewBottomConstraint.constant = 0; +} + +- (void)dismissKeyboard { + // Hide the keyboard + [_messageTextField resignFirstResponder]; + [_roomNameTextField resignFirstResponder]; +} + +#pragma mark - UITableView data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + // Check table view members vs messages + if (tableView == self.membersTableView) + { + return members.count; + } + + return messages.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + // Check table view members vs messages + if (tableView == self.membersTableView) + { + return 50; + } + + // Compute here height of message cells + CGFloat rowHeight; + // Get event related to this row + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + MXEvent *mxEvent = [messages objectAtIndex:indexPath.row]; + + // Check whether the cell will display an attachment or a text message + CGSize contentSize; + NSString *displayText = nil; + if ([mxHandler isAttachment:mxEvent]) { + contentSize = [self attachmentContentSize:mxEvent]; + if (!contentSize.width || !contentSize.height) { + // Check whether unsupported/unexpected messages should be exposed + if ([AppSettings sharedSettings].hideUnsupportedMessages) { + displayText = @""; + } else { + displayText = [NSString stringWithFormat:@"%@%@", kMatrixHandlerUnsupportedMessagePrefix, mxEvent.description]; + } + } + } else { + displayText = [mxHandler displayTextFor:mxEvent inSubtitleMode:NO]; + } + if (displayText) { + contentSize = [self textContentSize:displayText]; + } + + // Check whether the previous message has been sent by the same user. + // We group together messages from the same user. The user's picture and name are displayed only for the first message. + // We consider a new chunk when the user is different from the previous message's one. + BOOL isNewChunk = YES; + if (indexPath.row) { + MXEvent *previousMxEvent = [messages objectAtIndex:indexPath.row - 1]; + if ([previousMxEvent.userId isEqualToString:mxEvent.userId]) { + isNewChunk = NO; + } + } + + // Adjust cell height inside chunk + rowHeight = contentSize.height; + if (isNewChunk) { + // The cell is the first cell of the chunk + rowHeight += ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_DEFAULT; + } else { + // Inside chunk the height of the cell is reduced in order to reduce padding between messages + rowHeight += ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_IN_CHUNK; + } + + // Check whether the message is the last message of the current chunk + BOOL isChunkEnd = YES; + if (indexPath.row < messages.count - 1) { + MXEvent *nextMxEvent = [messages objectAtIndex:indexPath.row + 1]; + if ([nextMxEvent.userId isEqualToString:mxEvent.userId]) { + isChunkEnd = NO; + } + } + + if (!isNewChunk && !isChunkEnd) { + // Reduce again cell height to reduce space with the next cell + rowHeight += ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_GROUPED_CELL; + } else { + // The cell is the first cell of the chunk or the last one + rowHeight += ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_DEFAULT; + } + + if (isNewChunk && isChunkEnd) { + // When the chunk is composed by only one message, we consider the minimun cell height (50) in order to display correctly user's picture + if (rowHeight < 50) { + rowHeight = 50; + } + } + + return rowHeight; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + + // Check table view members vs messages + if (tableView == self.membersTableView) { + RoomMemberTableCell *memberCell = [tableView dequeueReusableCellWithIdentifier:@"RoomMemberCell" forIndexPath:indexPath]; + if (indexPath.row < members.count) { + [memberCell setRoomMember:[members objectAtIndex:indexPath.row] withRoom:mxRoom]; + } + + return memberCell; + } + + // Handle here room message cells + RoomMessageTableCell *cell; + MXEvent *mxEvent = [messages objectAtIndex:indexPath.row]; + BOOL isIncomingMsg = NO; + BOOL enableLinkDetection = YES; + + if ([mxEvent.userId isEqualToString:mxHandler.userId]) { + cell = [tableView dequeueReusableCellWithIdentifier:@"OutgoingMessageCell" forIndexPath:indexPath]; + [((OutgoingMessageTableCell*)cell).activityIndicator stopAnimating]; + // Restore initial settings of text view + if (initialAttributedStringForOutgoingMessage == nil) { + initialAttributedStringForOutgoingMessage = cell.messageTextView.attributedText; + } else { + cell.messageTextView.attributedText = initialAttributedStringForOutgoingMessage; + cell.messageTextView.dataDetectorTypes = UIDataDetectorTypeNone; + } + } else { + cell = [tableView dequeueReusableCellWithIdentifier:@"IncomingMessageCell" forIndexPath:indexPath]; + isIncomingMsg = YES; + // Restore initial settings of text view + if (initialAttributedStringForIncomingMessage == nil) { + initialAttributedStringForIncomingMessage = cell.messageTextView.attributedText; + } else { + cell.messageTextView.attributedText = initialAttributedStringForIncomingMessage; + cell.messageTextView.dataDetectorTypes = UIDataDetectorTypeNone; + } + } + + // Restore initial settings of attachment ImageView + cell.attachmentView.imageURL = nil; // Cancel potential attachment loading + cell.attachmentView.hidden = YES; + cell.playIconView.hidden = YES; + // Remove all gesture recognizer + while (cell.attachmentView.gestureRecognizers.count) { + [cell.attachmentView removeGestureRecognizer:cell.attachmentView.gestureRecognizers[0]]; + } + cell.attachmentViewTopAlignmentConstraint.constant = 0; + cell.attachmentViewBottomAlignmentConstraint.constant = 0; + + // Check whether the previous message has been sent by the same user. + // We group together messages from the same user. The user's picture and name are displayed only for the first message. + // We consider a new chunk when the user is different from the previous message's one. + BOOL isNewChunk = YES; + if (indexPath.row) { + MXEvent *previousMxEvent = [messages objectAtIndex:indexPath.row - 1]; + if ([previousMxEvent.userId isEqualToString:mxEvent.userId]) { + isNewChunk = NO; + } + } + + if (isNewChunk) { + // Adjust display of the first message of a chunk + cell.pictureView.hidden = NO; + cell.msgTextViewTopConstraint.constant = ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_DEFAULT; + cell.msgTextViewBottomConstraint.constant = ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_DEFAULT; + cell.messageTextView.contentInset = UIEdgeInsetsZero; + + // Set user's picture + cell.pictureView.placeholder = @"default-profile"; + cell.pictureView.imageURL = [mxRoom.state memberWithUserId:mxEvent.userId].avatarUrl; + [cell.pictureView.layer setCornerRadius:cell.pictureView.frame.size.width / 2]; + cell.pictureView.clipsToBounds = YES; + } else { + // Adjust display of other messages of the chunk + cell.pictureView.hidden = YES; + // The height of this cell has been reduced in order to reduce padding between messages of the same chunk + // We define here a negative constant for the top space between textView and its superview to display correctly the message text. + cell.msgTextViewTopConstraint.constant = ROOM_MESSAGE_CELL_TEXTVIEW_TOP_CONST_IN_CHUNK; + // Shift to the top the displayed message to reduce space with the previous messages + UIEdgeInsets edgeInsets = UIEdgeInsetsZero; + edgeInsets.top = ROOM_MESSAGE_CELL_TEXTVIEW_EDGE_INSET_TOP_IN_CHUNK; + cell.messageTextView.contentInset = edgeInsets; + + // Check whether the next message belongs to the same chunk in order to define bottom space between textView and its superview + cell.msgTextViewBottomConstraint.constant = ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_DEFAULT; + if (indexPath.row < messages.count - 1) { + MXEvent *nextMxEvent = [messages objectAtIndex:indexPath.row + 1]; + if ([nextMxEvent.userId isEqualToString:mxEvent.userId]) { + cell.msgTextViewBottomConstraint.constant = ROOM_MESSAGE_CELL_TEXTVIEW_BOTTOM_CONST_GROUPED_CELL; + } + } + } + + // Update incoming/outgoing message layout + if (isIncomingMsg) { + IncomingMessageTableCell* incomingMsgCell = (IncomingMessageTableCell*)cell; + // Display user's display name for the first meesage of a chunk, except if the name appears in the displayed text (see emote and membership event) + if (isNewChunk && [mxHandler isNotification:mxEvent] == NO) { + incomingMsgCell.userNameLabel.hidden = NO; + incomingMsgCell.userNameLabel.text = [mxRoom.state memberName:mxEvent.userId]; + } else { + incomingMsgCell.userNameLabel.hidden = YES; + } + + // Reset text color + cell.messageTextView.textColor = [UIColor blackColor]; + } else { + OutgoingMessageTableCell* outgoingMsgCell = (OutgoingMessageTableCell*)cell; + // Hide unsent label by default + outgoingMsgCell.unsentLabel.hidden = YES; + + // Set the right text color for outgoing messages + if ([mxEvent.eventId hasPrefix:kLocalEchoEventIdPrefix]) { + cell.messageTextView.textColor = [UIColor lightGrayColor]; + enableLinkDetection = NO; + } else if ([mxEvent.eventId hasPrefix:kFailedEventId]) { + cell.messageTextView.textColor = [UIColor redColor]; + enableLinkDetection = NO; + outgoingMsgCell.unsentLabel.hidden = NO; + // Align unsent label with the textView + outgoingMsgCell.unsentLabelTopConstraint.constant = cell.msgTextViewTopConstraint.constant + cell.messageTextView.contentInset.top - ROOM_MESSAGE_CELL_TEXTVIEW_EDGE_INSET_TOP_IN_CHUNK; + } else { + cell.messageTextView.textColor = [UIColor blackColor]; + } + } + + if ([mxHandler isAttachment:mxEvent]) { + cell.messageTextView.text = nil; // Note: Text view is used as attachment background view + CGSize contentSize = [self attachmentContentSize:mxEvent]; + if (!contentSize.width || !contentSize.height) { + NSLog(@"ERROR: Unsupported message %@", mxEvent.description); + // Check whether unsupported/unexpected messages should be exposed + if ([AppSettings sharedSettings].hideUnsupportedMessages == NO) { + // Display event content as unsupported message + cell.messageTextView.text = [NSString stringWithFormat:@"%@%@", kMatrixHandlerUnsupportedMessagePrefix, mxEvent.description]; + cell.messageTextView.textColor = [UIColor redColor]; + enableLinkDetection = NO; + } + // Adjust constraint constant + cell.msgTextViewWidthConstraint.constant = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH; + } else { + cell.attachmentView.hidden = NO; + // Fade attachments during upload + if (isIncomingMsg == NO && [mxEvent.eventId hasPrefix:kLocalEchoEventIdPrefix]) { + cell.attachmentView.alpha = 0.5; + [((OutgoingMessageTableCell*)cell).activityIndicator startAnimating]; + } else { + cell.attachmentView.alpha = 1; + } + + NSString *msgtype = mxEvent.content[@"msgtype"]; + if ([msgtype isEqualToString:kMXMessageTypeImage] || [msgtype isEqualToString:kMXMessageTypeVideo]) { + NSString *url = nil; + if ([msgtype isEqualToString:kMXMessageTypeVideo]) { + cell.playIconView.hidden = NO; + if (mxEvent.content[@"info"]) { + url = mxEvent.content[@"info"][@"thumbnail_url"]; + } + } else { + url = mxEvent.content[@"thumbnail_url"]; + } + + if (url == nil) { + url = mxEvent.content[@"url"]; + } + cell.attachmentView.imageURL = url; + + // Add tap recognizer to open attachment + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showAttachmentView:)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [tap setDelegate:self]; + [cell.attachmentView addGestureRecognizer:tap]; + // Store attachment content description used in showAttachmentView: + cell.attachmentView.mediaInfo = mxEvent.content; + } else { + cell.attachmentView.imageURL = nil; + } + + // Adjust constraint constant + cell.msgTextViewWidthConstraint.constant = contentSize.width; + // Align attachment inside text view by considering text view edge inset + cell.attachmentViewTopAlignmentConstraint.constant = ROOM_MESSAGE_CELL_IMAGE_MARGIN + cell.messageTextView.contentInset.top; + cell.attachmentViewBottomAlignmentConstraint.constant = -ROOM_MESSAGE_CELL_IMAGE_MARGIN + cell.messageTextView.contentInset.top; + } + } else { + NSString *displayText = [mxHandler displayTextFor:mxEvent inSubtitleMode:NO]; + // Update text color according to text content + if ([displayText hasPrefix:kMatrixHandlerUnsupportedMessagePrefix]) { + cell.messageTextView.textColor = [UIColor redColor]; + enableLinkDetection = NO; + } else if (isIncomingMsg && ([displayText rangeOfString:mxHandler.userDisplayName options:NSCaseInsensitiveSearch].location != NSNotFound || [displayText rangeOfString:mxHandler.userId options:NSCaseInsensitiveSearch].location != NSNotFound)) { + cell.messageTextView.textColor = [UIColor blueColor]; + } + cell.messageTextView.text = displayText; + // Adjust textView width constraint + cell.msgTextViewWidthConstraint.constant = [self textContentSize:displayText].width; + } + + // Turn on link detection only when it is usefull + if (enableLinkDetection) { + cell.messageTextView.dataDetectorTypes = UIDataDetectorTypeLink; + } + + // Handle timestamp display + if (dateFormatter && mxEvent.originServerTs) { + cell.dateTimeLabel.hidden = NO; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:mxEvent.originServerTs/1000]; + cell.dateTimeLabel.text = [dateFormatter stringFromDate:date]; + // Align dateTime label with the textView + cell.dateTimeLabelTopConstraint.constant = cell.msgTextViewTopConstraint.constant + cell.messageTextView.contentInset.top - ROOM_MESSAGE_CELL_TEXTVIEW_EDGE_INSET_TOP_IN_CHUNK; + } else { + cell.dateTimeLabel.hidden = YES; + } + + return cell; +} + +- (CGSize)textContentSize:(NSString*)textMsg { + // Use a TextView template to compute cell height + UITextView *dummyTextView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH, MAXFLOAT)]; + dummyTextView.font = [UIFont systemFontOfSize:14]; + dummyTextView.text = textMsg; + return [dummyTextView sizeThatFits:dummyTextView.frame.size]; +} + +- (CGSize)attachmentContentSize:(MXEvent*)mxEvent { + CGSize contentSize; + NSString *msgtype = mxEvent.content[@"msgtype"]; + if ([msgtype isEqualToString:kMXMessageTypeImage] || [msgtype isEqualToString:kMXMessageTypeVideo]) { + CGFloat width, height; + width = height = 0; + + NSDictionary *thumbInfo = nil; + if ([msgtype isEqualToString:kMXMessageTypeVideo]) { + if (mxEvent.content[@"info"]) { + thumbInfo = mxEvent.content[@"info"][@"thumbnail_info"]; + } + } else { + thumbInfo = mxEvent.content[@"thumbnail_info"]; + } + + if (thumbInfo) { + width = [thumbInfo[@"w"] integerValue] + 2 * ROOM_MESSAGE_CELL_IMAGE_MARGIN; + height = [thumbInfo[@"h"] integerValue] + 2 * ROOM_MESSAGE_CELL_IMAGE_MARGIN; + if (width > ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH || height > ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH) { + if (width > height) { + height = (height * ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH) / width; + height = floorf(height / 2) * 2; + width = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH; + } else { + width = (width * ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH) / height; + width = floorf(width / 2) * 2; + height = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH; + } + } + } + contentSize = CGSizeMake(width, height); + } else { + contentSize = CGSizeMake(40, 40); + } + return contentSize; +} + +#pragma mark - UITableView delegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + // Check table view members vs messages + if (tableView == self.membersTableView) { + // List action(s) available on this member + // TODO: Check user's power level before allowing an action (kick, ban, ...) + MXRoomMember *roomMember = [members objectAtIndex:indexPath.row]; + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + __weak typeof(self) weakSelf = self; + if (self.actionMenu) { + [self.actionMenu dismiss:NO]; + self.actionMenu = nil; + } + + // Consider the case of the user himself + if ([roomMember.userId isEqualToString:mxHandler.userId]) { + self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet]; + [self.actionMenu addActionWithTitle:@"Leave" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + MXRoom *currentRoom = [[MatrixHandler sharedHandler].mxSession room:weakSelf.roomId]; + [currentRoom leave:^{ + // Back to recents + [weakSelf.navigationController popViewControllerAnimated:YES]; + } failure:^(NSError *error) { + NSLog(@"Leave room %@ failed: %@", weakSelf.roomId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + } else { + // Consider membership of the selected member + switch (roomMember.membership) { + case MXMembershipInvite: + case MXMembershipJoin: { + self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet]; + [self.actionMenu addActionWithTitle:@"Kick" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + [[MatrixHandler sharedHandler].mxRestClient kickUser:roomMember.userId + fromRoom:weakSelf.roomId + reason:nil + success:^{ + } + failure:^(NSError *error) { + NSLog(@"Kick %@ failed: %@", roomMember.userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + [self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + [[MatrixHandler sharedHandler].mxRestClient banUser:roomMember.userId + inRoom:weakSelf.roomId + reason:nil + success:^{ + } + failure:^(NSError *error) { + NSLog(@"Ban %@ failed: %@", roomMember.userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + break; + } + case MXMembershipLeave: { + self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet]; + [self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + [[MatrixHandler sharedHandler].mxRestClient inviteUser:roomMember.userId + toRoom:weakSelf.roomId + success:^{ + } + failure:^(NSError *error) { + NSLog(@"Invite %@ failed: %@", roomMember.userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + [self.actionMenu addActionWithTitle:@"Ban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + [[MatrixHandler sharedHandler].mxRestClient banUser:roomMember.userId + inRoom:weakSelf.roomId + reason:nil + success:^{ + } + failure:^(NSError *error) { + NSLog(@"Ban %@ failed: %@", roomMember.userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + break; + } + case MXMembershipBan: { + self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet]; + [self.actionMenu addActionWithTitle:@"Unban" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + [[MatrixHandler sharedHandler].mxRestClient unbanUser:roomMember.userId + inRoom:weakSelf.roomId + success:^{ + } + failure:^(NSError *error) { + NSLog(@"Unban %@ failed: %@", roomMember.userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + break; + } + default: { + break; + } + } + } + + // Display the action sheet (if any) + if (self.actionMenu) { + self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + weakSelf.actionMenu = nil; + }]; + [self.actionMenu showInViewController:self]; + } + + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + } else if (tableView == self.messagesTableView) { + // Dismiss keyboard when user taps on messages table view content + [self dismissKeyboard]; + } +} + +// Detect vertical bounce at the top of the tableview to trigger pagination +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { + if (scrollView == self.messagesTableView) { + // paginate ? + if (scrollView.contentOffset.y < -64) + { + [self triggerBackPagination]; + } + } +} + +#pragma mark - UITextField delegate + +- (void)onTextFieldChange:(NSNotification *)notif { + NSString *msg = _messageTextField.text; + + if (msg.length) { + _sendBtn.enabled = YES; + _sendBtn.alpha = 1; + // Reset potential placeholder (used in case of wrong command usage) + _messageTextField.placeholder = nil; + } else { + _sendBtn.enabled = NO; + _sendBtn.alpha = 0.5; + } +} + +- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { + if (textField == self.roomNameTextField) { + self.roomNameTextField.borderStyle = UITextBorderStyleRoundedRect; + self.roomNameTextField.backgroundColor = [UIColor whiteColor]; + } + return YES; +} + +- (void)textFieldDidEndEditing:(UITextField *)textField { + if (textField == self.roomNameTextField) { + self.roomNameTextField.borderStyle = UITextBorderStyleNone; + self.roomNameTextField.backgroundColor = [UIColor clearColor]; + } +} + +- (BOOL)textFieldShouldReturn:(UITextField*) textField { + // "Done" key has been pressed + [textField resignFirstResponder]; + + if (textField == self.roomNameTextField) { + NSString *roomName = self.roomNameTextField.text; + if ([roomName isEqualToString:mxRoom.state.name] == NO) { + [self.activityIndicator startAnimating]; + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient setRoomName:self.roomId name:roomName success:^{ + if (isBackPaginationInProgress == NO) { + [self.activityIndicator stopAnimating]; + } + } failure:^(NSError *error) { + if (isBackPaginationInProgress == NO) { + [self.activityIndicator stopAnimating]; + } + // Revert change + self.roomNameTextField.text = mxRoom.state.displayname; + NSLog(@"Rename room failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + } + return YES; +} + +#pragma mark - Actions + +- (IBAction)onButtonPressed:(id)sender { + if (sender == _sendBtn) { + NSString *msgTxt = self.messageTextField.text; + + // Handle potential commands in room chat + if ([self isIRCStyleCommand:msgTxt] == NO) { + [self postTextMessage:msgTxt]; + } + + self.messageTextField.text = nil; + // disable send button + [self onTextFieldChange:nil]; + } else if (sender == _optionBtn) { + [self dismissKeyboard]; + + // Display action menu: Add attachments, Invite user... + __weak typeof(self) weakSelf = self; + self.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an action:" message:nil style:CustomAlertStyleActionSheet]; + // Attachments + [self.actionMenu addActionWithTitle:@"Attach" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + // Ask for attachment type + weakSelf.actionMenu = [[CustomAlert alloc] initWithTitle:@"Select an attachment type:" message:nil style:CustomAlertStyleActionSheet]; + [weakSelf.actionMenu addActionWithTitle:@"Media" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + weakSelf.actionMenu = nil; + // Open media gallery + UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init]; + mediaPicker.delegate = weakSelf; + mediaPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + mediaPicker.allowsEditing = NO; + mediaPicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeMovie, nil]; + [[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker]; + } + }]; + weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + weakSelf.actionMenu = nil; + }]; + [weakSelf.actionMenu showInViewController:weakSelf]; + } + }]; + // Invitation + [self.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + if (weakSelf) { + // Ask for userId to invite + weakSelf.actionMenu = [[CustomAlert alloc] initWithTitle:@"User ID:" message:nil style:CustomAlertStyleAlert]; + weakSelf.actionMenu.cancelButtonIndex = [weakSelf.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + weakSelf.actionMenu = nil; + }]; + [weakSelf.actionMenu addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.secureTextEntry = NO; + textField.placeholder = @"ex: @bob:homeserver"; + }]; + [weakSelf.actionMenu addActionWithTitle:@"Invite" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + UITextField *textField = [alert textFieldAtIndex:0]; + NSString *userId = textField.text; + weakSelf.actionMenu = nil; + if (userId.length) { + [[MatrixHandler sharedHandler].mxRestClient inviteUser:userId toRoom:weakSelf.roomId success:^{ + + } failure:^(NSError *error) { + NSLog(@"Invite %@ failed: %@", userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + [weakSelf.actionMenu showInViewController:weakSelf]; + } + }]; + self.actionMenu.cancelButtonIndex = [self.actionMenu addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + weakSelf.actionMenu = nil; + }]; + weakSelf.actionMenu.sourceView = weakSelf.optionBtn; + [self.actionMenu showInViewController:self]; + } +} + +- (IBAction)showHideDateTime:(id)sender { + if (dateFormatter) { + // dateTime will be hidden + dateFormatter = nil; + } else { + // dateTime will be visible + NSString *dateFormat = @"MMM dd HH:mm"; + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]]]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [dateFormatter setDateFormat:dateFormat]; + } + + [self.messagesTableView reloadData]; +} + +#pragma mark - Post messages + +- (void)postMessage:(NSDictionary*)msgContent withLocalEventId:(NSString*)localEventId { + MXMessageType msgType = msgContent[@"msgtype"]; + if (msgType) { + // Check whether a temporary event has already been added for local echo (this happens on attachments) + MXEvent *mxEvent = nil; + if (localEventId) { + // Update the temporary event with the actual msg content + NSUInteger index = messages.count; + while (index--) { + mxEvent = [messages objectAtIndex:index]; + if ([mxEvent.eventId isEqualToString:localEventId]) { + mxEvent.content = msgContent; + // Refresh table display + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + break; + } + } + } else { + // Create a temporary event to displayed outgoing message (local echo) + localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]]; + mxEvent = [[MXEvent alloc] init]; + mxEvent.roomId = self.roomId; + mxEvent.eventId = localEventId; + mxEvent.eventType = MXEventTypeRoomMessage; + mxEvent.type = kMXEventTypeStringRoomMessage; + mxEvent.content = msgContent; + mxEvent.userId = [MatrixHandler sharedHandler].userId; + // Update table sources + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0]; + [messages addObject:mxEvent]; + // Refresh table display (Disable animation during cells insertion to prevent flickering) + [UIView setAnimationsEnabled:NO]; + [self.messagesTableView beginUpdates]; + if (indexPath.row > 0) { + NSIndexPath *prevIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[prevIndexPath] withRowAnimation:UITableViewRowAnimationNone]; + } + [self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + [self.messagesTableView endUpdates]; + [UIView setAnimationsEnabled:YES]; + + [self scrollToBottomAnimated:NO]; + } + + // Send message to the room + [[[MatrixHandler sharedHandler] mxRestClient] postMessageToRoom:self.roomId msgType:msgType content:mxEvent.content success:^(NSString *event_id) { + // Update the temporary event with the actual event id + NSUInteger index = messages.count; + while (index--) { + MXEvent *mxEvent = [messages objectAtIndex:index]; + if ([mxEvent.eventId isEqualToString:localEventId]) { + mxEvent.eventId = event_id; + // Refresh table display + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + break; + } + } + } failure:^(NSError *error) { + [self handleError:error forLocalEventId:localEventId]; + }]; + } +} + +- (void)postTextMessage:(NSString*)msgTxt { + MXMessageType msgType = kMXMessageTypeText; + // Check whether the message is an emote + if ([msgTxt hasPrefix:@"/me "]) { + msgType = kMXMessageTypeEmote; + // Remove "/me " string + msgTxt = [msgTxt substringFromIndex:4]; + } + + [self postMessage:@{@"msgtype":msgType, @"body":msgTxt} withLocalEventId:nil]; +} + +- (NSString*)addLocalEventForAttachedImage:(UIImage*)image { + // Create a temporary event to displayed outgoing message (local echo) + NSString *localEventId = [NSString stringWithFormat:@"%@%@", kLocalEchoEventIdPrefix, [[NSProcessInfo processInfo] globallyUniqueString]]; + MXEvent *mxEvent = [[MXEvent alloc] init]; + mxEvent.roomId = self.roomId; + mxEvent.eventId = localEventId; + mxEvent.eventType = MXEventTypeRoomMessage; + mxEvent.type = kMXEventTypeStringRoomMessage; + // We store temporarily the image in cache, use the localId to build temporary url + NSString *dummyURL = [NSString stringWithFormat:@"%@%@", kMediaManagerPrefixForDummyURL, localEventId]; + NSData *imageData = UIImageJPEGRepresentation(image, 0.5); + NSString *cacheFilePath = [MediaManager cacheMediaData:imageData forURL:dummyURL mimeType:@"image/jpeg"]; + if (cacheFilePath) { + if (tmpCachedAttachments == nil) { + tmpCachedAttachments = [NSMutableArray array]; + } + [tmpCachedAttachments addObject:cacheFilePath]; + } + NSMutableDictionary *thumbnailInfo = [[NSMutableDictionary alloc] init]; + [thumbnailInfo setValue:@"image/jpeg" forKey:@"mimetype"]; + [thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)image.size.width] forKey:@"w"]; + [thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)image.size.height] forKey:@"h"]; + [thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:imageData.length] forKey:@"size"]; + mxEvent.content = @{@"msgtype":@"m.image", @"thumbnail_info":thumbnailInfo, @"thumbnail_url":dummyURL}; + mxEvent.userId = [MatrixHandler sharedHandler].userId; + + // Update table sources + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:messages.count inSection:0]; + [messages addObject:mxEvent]; + + // Refresh table display (Disable animation during cells insertion to prevent flickering) + [UIView setAnimationsEnabled:NO]; + [self.messagesTableView beginUpdates]; + if (indexPath.row > 0) { + NSIndexPath *prevIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[prevIndexPath] withRowAnimation:UITableViewRowAnimationNone]; + } + [self.messagesTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + [self.messagesTableView endUpdates]; + [UIView setAnimationsEnabled:YES]; + + [self scrollToBottomAnimated:NO]; + return localEventId; +} + +- (void)handleError:(NSError *)error forLocalEventId:(NSString *)localEventId { + NSLog(@"Post message failed: %@", error); + if (error) { + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + } + + // Update the temporary event with this local event id + NSUInteger index = messages.count; + while (index--) { + MXEvent *mxEvent = [messages objectAtIndex:index]; + if ([mxEvent.eventId isEqualToString:localEventId]) { + NSLog(@"Posted event: %@", mxEvent.description); + mxEvent.eventId = kFailedEventId; + // Refresh table display + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; + [self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + break; + } + } +} + +- (BOOL)isIRCStyleCommand:(NSString*)text{ + // Check whether the provided text may be an IRC-style command + if ([text hasPrefix:@"/"] == NO || [text hasPrefix:@"//"] == YES) { + return NO; + } + + // Parse command line + NSArray *components = [text componentsSeparatedByString:@" "]; + NSString *cmd = [components objectAtIndex:0]; + NSUInteger index = 1; + + if ([cmd isEqualToString:kCmdEmote]) { + // post message as an emote + [self postTextMessage:text]; + } else if ([text hasPrefix:kCmdChangeDisplayName]) { + // Change display name + NSString *displayName = [text substringFromIndex:kCmdChangeDisplayName.length + 1]; + // Remove white space from both ends + displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + if (displayName.length) { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient setDisplayName:displayName success:^{ + } failure:^(NSError *error) { + NSLog(@"Set displayName failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /nick "; + } + } else if ([text hasPrefix:kCmdJoinRoom]) { + // Join a room + NSString *roomAlias = [text substringFromIndex:kCmdJoinRoom.length + 1]; + // Remove white space from both ends + roomAlias = [roomAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + // Check + if (roomAlias.length) { + // FIXME + NSLog(@"Join Alias is not supported yet (%@)", text); + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/join is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; + [alert show]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /join "; + } + } else { + // Retrieve userId + NSString *userId = nil; + while (index < components.count) { + userId = [components objectAtIndex:index++]; + if (userId.length) { + // done + break; + } + // reset + userId = nil; + } + + if ([cmd isEqualToString:kCmdKickUser]) { + if (userId) { + // Retrieve potential reason + NSString *reason = nil; + while (index < components.count) { + if (reason) { + reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]]; + } else { + reason = [components objectAtIndex:index++]; + } + } + // Kick the user + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient kickUser:userId fromRoom:self.roomId reason:reason success:^{ + } failure:^(NSError *error) { + NSLog(@"Kick user (%@) failed: %@", userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /kick []"; + } + } else if ([cmd isEqualToString:kCmdBanUser]) { + if (userId) { + // Retrieve potential reason + NSString *reason = nil; + while (index < components.count) { + if (reason) { + reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]]; + } else { + reason = [components objectAtIndex:index++]; + } + } + // Ban the user + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient banUser:userId inRoom:self.roomId reason:reason success:^{ + } failure:^(NSError *error) { + NSLog(@"Ban user (%@) failed: %@", userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /ban []"; + } + } else if ([cmd isEqualToString:kCmdUnbanUser]) { + if (userId) { + // Unban the user + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient unbanUser:userId inRoom:self.roomId success:^{ + } failure:^(NSError *error) { + NSLog(@"Unban user (%@) failed: %@", userId, error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /unban "; + } + } else if ([cmd isEqualToString:kCmdSetUserPowerLevel]) { + // Retrieve power level + NSString *powerLevel = nil; + while (index < components.count) { + powerLevel = [components objectAtIndex:index++]; + if (powerLevel.length) { + // done + break; + } + // reset + powerLevel = nil; + } + // Set power level + if (userId && powerLevel) { + // FIXME + NSLog(@"Set user power level (/op) is not supported yet (%@)", userId); + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/op is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; + [alert show]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /op "; + } + } else if ([cmd isEqualToString:kCmdResetUserPowerLevel]) { + if (userId) { + // Reset user power level + // FIXME + NSLog(@"Reset user power level (/deop) is not supported yet (%@)", userId); + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"/deop is not supported yet" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil]; + [alert show]; + } else { + // Display cmd usage in text input as placeholder + self.messageTextField.placeholder = @"Usage: /deop "; + } + } else { + NSLog(@"Unrecognised IRC-style command: %@", text); + self.messageTextField.placeholder = [NSString stringWithFormat:@"Unrecognised IRC-style command: %@", cmd]; + } + } + return YES; +} + +# pragma mark - UIImagePickerControllerDelegate + +- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { + NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType]; + if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { + UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage]; + if (selectedImage) { + NSString * localEventId = [self addLocalEventForAttachedImage:selectedImage]; + // Upload image and its thumbnail + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + NSUInteger thumbnailSize = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH - 2 * ROOM_MESSAGE_CELL_IMAGE_MARGIN; + [mxHandler.mxRestClient uploadImage:selectedImage thumbnailSize:thumbnailSize timeout:30 success:^(NSDictionary *imageMessage) { + // Send image + [self postMessage:imageMessage withLocalEventId:localEventId]; + } failure:^(NSError *error) { + [self handleError:error forLocalEventId:localEventId]; + }]; + } + } else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) { + NSURL* selectedVideo = [info objectForKey:UIImagePickerControllerMediaURL]; + if (selectedVideo) { + // Create video thumbnail + MPMoviePlayerController* moviePlayerController = [[MPMoviePlayerController alloc] initWithContentURL:selectedVideo]; + if (moviePlayerController) { + [moviePlayerController setShouldAutoplay:NO]; + UIImage* videoThumbnail = [moviePlayerController thumbnailImageAtTime:(NSTimeInterval)1 timeOption:MPMovieTimeOptionNearestKeyFrame]; + [moviePlayerController stop]; + moviePlayerController = nil; + + if (videoThumbnail) { + // Prepare video thumbnail description + NSUInteger thumbnailSize = ROOM_MESSAGE_CELL_MAX_TEXTVIEW_WIDTH - 2 * ROOM_MESSAGE_CELL_IMAGE_MARGIN; + UIImage *thumbnail = [MediaManager resize:videoThumbnail toFitInSize:CGSizeMake(thumbnailSize, thumbnailSize)]; + NSMutableDictionary *thumbnailInfo = [[NSMutableDictionary alloc] init]; + [thumbnailInfo setValue:@"image/jpeg" forKey:@"mimetype"]; + [thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)thumbnail.size.width] forKey:@"w"]; + [thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)thumbnail.size.height] forKey:@"h"]; + NSData *thumbnailData = UIImageJPEGRepresentation(thumbnail, 0.9); + [thumbnailInfo setValue:[NSNumber numberWithUnsignedInteger:thumbnailData.length] forKey:@"size"]; + + // Create the local event displayed during uploading + NSString * localEventId = [self addLocalEventForAttachedImage:thumbnail]; + + // Upload thumbnail + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient uploadContent:thumbnailData mimeType:@"image/jpeg" timeout:30 success:^(NSString *url) { + // Prepare content of attached video + NSMutableDictionary *videoContent = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *videoInfo = [[NSMutableDictionary alloc] init]; + [videoContent setValue:@"m.video" forKey:@"msgtype"]; + [videoInfo setValue:url forKey:@"thumbnail_url"]; + [videoInfo setValue:thumbnailInfo forKey:@"thumbnail_info"]; + + // Convert video container to mp4 + AVURLAsset* videoAsset = [AVURLAsset URLAssetWithURL:selectedVideo options:nil]; + AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:videoAsset presetName:AVAssetExportPresetMediumQuality]; + // Set output URL + NSString * outputFileName = [NSString stringWithFormat:@"%.0f.mp4",[[NSDate date] timeIntervalSince1970]]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *cacheRoot = [paths objectAtIndex:0]; + NSURL *tmpVideoLocation = [NSURL fileURLWithPath:[cacheRoot stringByAppendingPathComponent:outputFileName]]; + exportSession.outputURL = tmpVideoLocation; + // Check supported output file type + NSArray *supportedFileTypes = exportSession.supportedFileTypes; + if ([supportedFileTypes containsObject:AVFileTypeMPEG4]) { + exportSession.outputFileType = AVFileTypeMPEG4; + [videoInfo setValue:@"video/mp4" forKey:@"mimetype"]; + } else { + NSLog(@"Unexpected case: MPEG-4 file format is not supported"); + // we send QuickTime movie file by default + exportSession.outputFileType = AVFileTypeQuickTimeMovie; + [videoInfo setValue:@"video/quicktime" forKey:@"mimetype"]; + } + // Export video file and send it + [exportSession exportAsynchronouslyWithCompletionHandler:^{ + // Check status + if ([exportSession status] == AVAssetExportSessionStatusCompleted) { + AVURLAsset* asset = [AVURLAsset URLAssetWithURL:tmpVideoLocation + options:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], + AVURLAssetPreferPreciseDurationAndTimingKey, + nil] + ]; + + [videoInfo setValue:[NSNumber numberWithDouble:(1000 * CMTimeGetSeconds(asset.duration))] forKey:@"duration"]; + NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo]; + if (videoTracks.count > 0) { + AVAssetTrack *videoTrack = [videoTracks objectAtIndex:0]; + CGSize videoSize = videoTrack.naturalSize; + [videoInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)videoSize.width] forKey:@"w"]; + [videoInfo setValue:[NSNumber numberWithUnsignedInteger:(NSUInteger)videoSize.height] forKey:@"h"]; + } + + // Upload the video + NSData *videoData = [NSData dataWithContentsOfURL:tmpVideoLocation]; + [[NSFileManager defaultManager] removeItemAtPath:[tmpVideoLocation path] error:nil]; + if (videoData) { + if (videoData.length < UPLOAD_FILE_SIZE) { + [videoInfo setValue:[NSNumber numberWithUnsignedInteger:videoData.length] forKey:@"size"]; + [mxHandler.mxRestClient uploadContent:videoData mimeType:videoInfo[@"mimetype"] timeout:30 success:^(NSString *url) { + [videoContent setValue:url forKey:@"url"]; + [videoContent setValue:videoInfo forKey:@"info"]; + [videoContent setValue:@"Video" forKey:@"body"]; + [self postMessage:videoContent withLocalEventId:localEventId]; + } failure:^(NSError *error) { + [self handleError:error forLocalEventId:localEventId]; + }]; + } else { + NSLog(@"Video is too large"); + [self handleError:nil forLocalEventId:localEventId]; + } + } else { + NSLog(@"Attach video failed: no data"); + [self handleError:nil forLocalEventId:localEventId]; + } + } + else { + NSLog(@"Video export failed: %d", [exportSession status]); + // remove tmp file (if any) + [[NSFileManager defaultManager] removeItemAtPath:[tmpVideoLocation path] error:nil]; + [self handleError:nil forLocalEventId:localEventId]; + } + }]; + } failure:^(NSError *error) { + NSLog(@"Video thumbnail upload failed"); + [self handleError:error forLocalEventId:localEventId]; + }]; + } + } + } + } + + [self dismissMediaPicker]; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { + [self dismissMediaPicker]; +} + +- (void)dismissMediaPicker { + [[AppDelegate theDelegate].masterTabBarController dismissMediaPicker]; +} +@end diff --git a/matrixConsole/ViewController/SettingsViewController.h b/matrixConsole/ViewController/SettingsViewController.h new file mode 100644 index 000000000..929b88734 --- /dev/null +++ b/matrixConsole/ViewController/SettingsViewController.h @@ -0,0 +1,24 @@ +/* + 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 + +@interface SettingsViewController : UITableViewController + +- (void)reset; + +@end + diff --git a/matrixConsole/ViewController/SettingsViewController.m b/matrixConsole/ViewController/SettingsViewController.m new file mode 100644 index 000000000..e1753b8a9 --- /dev/null +++ b/matrixConsole/ViewController/SettingsViewController.m @@ -0,0 +1,517 @@ +/* + 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 "SettingsViewController.h" + +#import "AppDelegate.h" +#import "AppSettings.h" +#import "MatrixHandler.h" +#import "MediaManager.h" + +#import "SettingsTableViewCell.h" + +#define SETTINGS_SECTION_NOTIFICATIONS_INDEX 0 +#define SETTINGS_SECTION_ROOMS_INDEX 1 +#define SETTINGS_SECTION_CONFIGURATION_INDEX 2 +#define SETTINGS_SECTION_COMMANDS_INDEX 3 + +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 : change your display name\r\n /me : send the action you are doing. /me will be replaced by your display name\r\n /join : join a room\r\n /kick []: kick the user\r\n /ban []: ban the user\r\n /unban : unban the user\r\n /op : set user power level\r\n /deop : reset user power level to the room default value"; + +@interface SettingsViewController () { + id imageLoader; + + NSString *currentDisplayName; + NSString *currentPictureURL; + NSString *uploadedPictureURL; + + NSMutableArray *errorAlerts; + + UIButton *logoutBtn; + UISwitch *notificationsSwitch; + UISwitch *allEventsSwitch; + UISwitch *unsupportedMsgSwitch; + UISwitch *sortMembersSwitch; +} +@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 (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; + +- (IBAction)onButtonPressed:(id)sender; + +@end + +@implementation SettingsViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + + // Add logout button in nav bar + logoutBtn = [UIButton buttonWithType:UIButtonTypeSystem]; + logoutBtn.frame = CGRectMake(0, 0, 60, 44); + [logoutBtn setTitle:@"Logout" forState:UIControlStateNormal]; + [logoutBtn setTitle:@"Logout" forState:UIControlStateHighlighted]; + [logoutBtn addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:logoutBtn]; + + errorAlerts = [NSMutableArray array]; + + [self startViewConfiguration]; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. + + if (imageLoader) { + [MediaManager cancel:imageLoader]; + imageLoader = nil; + } +} + +- (void)dealloc { + [[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"userDisplayName"]; + [[MatrixHandler sharedHandler] removeObserver:self forKeyPath:@"userPictureURL"]; + + // Cancel picture loader (if any) + if (imageLoader) { + [MediaManager cancel:imageLoader]; + imageLoader = nil; + } + + // Cancel potential error alerts + for (CustomAlert *alert in errorAlerts){ + [alert dismiss:NO]; + } + errorAlerts = nil; + + logoutBtn = nil; + notificationsSwitch = nil; + allEventsSwitch = nil; + unsupportedMsgSwitch = nil; + sortMembersSwitch = nil; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + // Refresh displayed settings + [self.tableView reloadData]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; +} + +#pragma mark - Internal methods + +- (void)reset { + // Cancel picture loader (if any) + if (imageLoader) { + [MediaManager cancel:imageLoader]; + imageLoader = nil; + } + + // Cancel potential error alerts + for (CustomAlert *alert in errorAlerts){ + [alert dismiss:NO]; + } + + currentPictureURL = nil; + uploadedPictureURL = nil; + UIImage *image = [UIImage imageNamed:@"default-profile"]; + [self.userPicture setImage:image forState:UIControlStateNormal]; + [self.userPicture setImage:image forState:UIControlStateHighlighted]; + + currentDisplayName = nil; + self.userDisplayName.text = nil; +} + +- (void)startViewConfiguration { + // Initialize + [self reset]; + + // Set current user's information and add observers + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [_activityIndicator startAnimating]; + // Disable user's interactions + _userPicture.enabled = NO; + _userDisplayName.enabled = NO; + + // Set user's display name + currentDisplayName = mxHandler.userDisplayName; + self.userDisplayName.text = mxHandler.userDisplayName; + [[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"userDisplayName" options:0 context:nil]; + [mxHandler.mxRestClient displayNameForUser:mxHandler.userId success:^(NSString *displayname) { + mxHandler.userDisplayName = displayname; + + // Set user's picture url + [self updateUserPicture:mxHandler.userPictureURL]; + [[MatrixHandler sharedHandler] addObserver:self forKeyPath:@"userPictureURL" options:0 context:nil]; + [mxHandler.mxRestClient avatarUrlForUser:mxHandler.userId success:^(NSString *avatar_url) { + mxHandler.userPictureURL = avatar_url; + [self endViewConfiguration]; + + } failure:^(NSError *error) { + NSLog(@"Get picture url failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + [self endViewConfiguration]; + }]; + } failure:^(NSError *error) { + NSLog(@"Get displayName failed: %@", error); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + [self endViewConfiguration]; + }]; +} + +- (void)endViewConfiguration { + [_activityIndicator stopAnimating]; + + _userPicture.enabled = YES; + _userDisplayName.enabled = YES; + + [self.tableView reloadData]; +} + +- (void)saveDisplayName { + // Check whether the display name has been changed + NSString *displayname = self.userDisplayName.text; + if ([displayname isEqualToString:currentDisplayName] == NO) { + // Save display name + [_activityIndicator startAnimating]; + _userDisplayName.enabled = NO; + + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [mxHandler.mxRestClient setDisplayName:displayname success:^{ + currentDisplayName = displayname; + + [_activityIndicator stopAnimating]; + _userDisplayName.enabled = YES; + } failure:^(NSError *error) { + NSLog(@"Set displayName failed: %@", error); + [_activityIndicator stopAnimating]; + _userDisplayName.enabled = YES; + + //Alert user + NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey]; + if (!title) { + title = @"Display name change failed"; + } + NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey]; + + CustomAlert *alert = [[CustomAlert alloc] initWithTitle:title message:msg style:CustomAlertStyleAlert]; + [errorAlerts addObject:alert]; + alert.cancelButtonIndex = [alert addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + [errorAlerts removeObject:alert]; + // Remove change + self.userDisplayName.text = currentDisplayName; + }]; + [alert addActionWithTitle:@"Retry" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + [errorAlerts removeObject:alert]; + [self saveDisplayName]; + }]; + [alert showInViewController:self]; + }]; + } +} + +- (void)savePicture { + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + + // Save picture + [_activityIndicator startAnimating]; + _userPicture.enabled = NO; + + 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]; + }]; + } else { + [mxHandler.mxRestClient setAvatarUrl:uploadedPictureURL + success:^{ + [MatrixHandler sharedHandler].userPictureURL = uploadedPictureURL; + uploadedPictureURL = nil; + + [_activityIndicator stopAnimating]; + _userPicture.enabled = YES; + } failure:^(NSError *error) { + NSLog(@"Set avatar url failed: %@", error); + [_activityIndicator stopAnimating]; + _userPicture.enabled = YES; + [self handleErrorDuringPictureSaving:error]; + }]; + } +} + +- (void)handleErrorDuringPictureSaving:(NSError*)error { + NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey]; + if (!title) { + title = @"Picture change failed"; + } + NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey]; + + CustomAlert *alert = [[CustomAlert alloc] initWithTitle:title message:msg style:CustomAlertStyleAlert]; + [errorAlerts addObject:alert]; + alert.cancelButtonIndex = [alert addActionWithTitle:@"Cancel" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + [errorAlerts removeObject:alert]; + // Remove change + uploadedPictureURL = nil; + [self updateUserPicture:[MatrixHandler sharedHandler].userPictureURL]; + }]; + [alert addActionWithTitle:@"Retry" style:CustomAlertActionStyleDefault handler:^(CustomAlert *alert) { + [errorAlerts removeObject:alert]; + [self savePicture]; + }]; + + [alert showInViewController:self]; +} + +- (void)updateUserPicture:(NSString *)avatar_url { + if (currentPictureURL == nil || [currentPictureURL isEqualToString:avatar_url] == NO) { + // Cancel previous loader (if any) + if (imageLoader) { + [MediaManager cancel:imageLoader]; + 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; + }]; + } else { + // Set placeholder + UIImage *image = [UIImage imageNamed:@"default-profile"]; + [self.userPicture setImage:image forState:UIControlStateNormal]; + [self.userPicture setImage:image forState:UIControlStateHighlighted]; + } + } +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([@"userDisplayName" isEqualToString:keyPath]) { + // Refresh user's display name + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + if ([currentDisplayName isEqualToString:mxHandler.userDisplayName] == NO) { + currentDisplayName = mxHandler.userDisplayName; + self.userDisplayName.text = mxHandler.userDisplayName; + } + } else if ([@"userPictureURL" isEqualToString:keyPath]) { + // Refresh user's picture + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + [self updateUserPicture:mxHandler.userPictureURL]; + } +} + +#pragma mark - Actions + +- (IBAction)onButtonPressed:(id)sender { + [self dismissKeyboard]; + + if (sender == _userPicture) { + // Open picture gallery + UIImagePickerController *mediaPicker = [[UIImagePickerController alloc] init]; + mediaPicker.delegate = self; + mediaPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + mediaPicker.allowsEditing = NO; + [[AppDelegate theDelegate].masterTabBarController presentMediaPicker:mediaPicker]; + } else if (sender == logoutBtn) { + [self reset]; + [[AppDelegate theDelegate] logout]; + } else if (sender == notificationsSwitch) { + [AppSettings sharedSettings].enableNotifications = notificationsSwitch.on; + } else if (sender == allEventsSwitch) { + [AppSettings sharedSettings].displayAllEvents = allEventsSwitch.on; + } else if (sender == unsupportedMsgSwitch) { + [AppSettings sharedSettings].hideUnsupportedMessages = unsupportedMsgSwitch.on; + } else if (sender == sortMembersSwitch) { + [AppSettings sharedSettings].sortMembersUsingLastSeenTime = sortMembersSwitch.on; + } +} + +#pragma mark - keyboard + +- (void)dismissKeyboard +{ + // Hide the keyboard + [_userDisplayName resignFirstResponder]; + // Save display name change (if any) + [self saveDisplayName]; +} + +#pragma mark - UITextField delegate + +- (BOOL)textFieldShouldReturn:(UITextField*) textField +{ + // "Done" key has been pressed + [self dismissKeyboard]; + return YES; +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 4; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + if (section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { + return 1; + } else if (section == SETTINGS_SECTION_ROOMS_INDEX) { + return 3; + } else if (section == SETTINGS_SECTION_CONFIGURATION_INDEX) { + return 1; + } else if (section == SETTINGS_SECTION_COMMANDS_INDEX) { + return 1; + } + + return 0; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { + return 44; + } else if (indexPath.section == SETTINGS_SECTION_ROOMS_INDEX) { + return 44; + } else if (indexPath.section == SETTINGS_SECTION_CONFIGURATION_INDEX) { + UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, MAXFLOAT)]; + textView.font = [UIFont systemFontOfSize:14]; + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + textView.text = [NSString stringWithFormat:kConfigurationFormatText, mxHandler.homeServerURL, nil, mxHandler.userId, mxHandler.accessToken]; + CGSize contentSize = [textView sizeThatFits:textView.frame.size]; + return contentSize.height + 1; + } else if (indexPath.section == SETTINGS_SECTION_COMMANDS_INDEX) { + UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, MAXFLOAT)]; + textView.font = [UIFont systemFontOfSize:14]; + textView.text = kCommandsDescriptionText; + CGSize contentSize = [textView sizeThatFits:textView.frame.size]; + return contentSize.height + 1; + } + + return 44; +} + +- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return 30; +} +- (CGFloat) tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section +{ + return 1; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + UILabel *sectionHeader = [[UILabel alloc] initWithFrame:[tableView rectForHeaderInSection:section]]; + sectionHeader.font = [UIFont boldSystemFontOfSize:16]; + sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0]; + + if (section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { + sectionHeader.text = @" Notifications"; + } else if (section == SETTINGS_SECTION_ROOMS_INDEX) { + sectionHeader.text = @" Rooms"; + } else if (section == SETTINGS_SECTION_CONFIGURATION_INDEX) { + sectionHeader.text = @" Configuration"; + } else if (section == SETTINGS_SECTION_COMMANDS_INDEX) { + sectionHeader.text = @" Commands"; + } else { + sectionHeader = nil; + } + return sectionHeader; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + SettingsTableViewCell *cell = nil; + + if (indexPath.section == SETTINGS_SECTION_NOTIFICATIONS_INDEX) { + SettingsTableCellWithSwitch *notificationsCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithSwitch" forIndexPath:indexPath]; + notificationsCell.settingLabel.text = @"Enable notifications"; + notificationsCell.settingSwitch.on = [[AppSettings sharedSettings] enableNotifications]; + notificationsSwitch = notificationsCell.settingSwitch; + 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 { + roomsSettingCell.settingLabel.text = @"Sort members by last seen time"; + roomsSettingCell.settingSwitch.on = [[AppSettings sharedSettings] sortMembersUsingLastSeenTime]; + sortMembersSwitch = roomsSettingCell.settingSwitch; + } + cell = roomsSettingCell; + } else if (indexPath.section == SETTINGS_SECTION_CONFIGURATION_INDEX) { + SettingsTableCellWithTextView *configCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath]; + MatrixHandler *mxHandler = [MatrixHandler sharedHandler]; + configCell.settingTextView.text = [NSString stringWithFormat:kConfigurationFormatText, mxHandler.homeServerURL, nil, mxHandler.userId, mxHandler.accessToken]; + cell = configCell; + } else if (indexPath.section == SETTINGS_SECTION_COMMANDS_INDEX) { + SettingsTableCellWithTextView *commandsCell = [tableView dequeueReusableCellWithIdentifier:@"SettingsCellWithTextView" forIndexPath:indexPath]; + commandsCell.settingTextView.text = kCommandsDescriptionText; + cell = commandsCell; + } + + return cell; +} + +# pragma mark - UIImagePickerControllerDelegate + +- (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 dismissMediaPicker]; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { + [self dismissMediaPicker]; +} + +- (void)dismissMediaPicker { + [[AppDelegate theDelegate].masterTabBarController dismissMediaPicker]; +} + +@end diff --git a/matrixConsole/main.m b/matrixConsole/main.m new file mode 100644 index 000000000..2e3f4608c --- /dev/null +++ b/matrixConsole/main.m @@ -0,0 +1,24 @@ +/* + 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 +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/matrixConsoleTests/Info.plist b/matrixConsoleTests/Info.plist new file mode 100644 index 000000000..dab962b2d --- /dev/null +++ b/matrixConsoleTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.matrix.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/matrixConsoleTests/matrixConsoleTests.m b/matrixConsoleTests/matrixConsoleTests.m new file mode 100644 index 000000000..c7463668b --- /dev/null +++ b/matrixConsoleTests/matrixConsoleTests.m @@ -0,0 +1,48 @@ +/* + 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 +#import + +@interface syMessagingTests : XCTestCase + +@end + +@implementation syMessagingTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample { + // This is an example of a functional test case. + XCTAssert(YES, @"Pass"); +} + +- (void)testPerformanceExample { + // This is an example of a performance test case. + [self measureBlock:^{ + // Put the code you want to measure the time of here. + }]; +} + +@end