diff --git a/CHANGES.rst b/CHANGES.rst index 4c8aca268..481f2d862 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,26 @@ +Changes in 0.4.0 (2017-06-16) +=============================================== + +Improvements: + * Upgrade MatrixKit version (v0.5.0). + * Full UX rework. + * Add read markers synchronisation across matrix clients. + * Add a new popup dialog for reporting bugs and crashes + * Add a picker to select a server directory. + * Add an option to join room by id or alias. + * Pods: Update Cocoapods and reduce Riot/OLM coupling, thanks to @hberenger (PR #1220). + +Bug fixes: + * Files search: display the attachment thumbnail (#1135). + * Chevron to exit roomview after clicking through from search results can disappear (#841). + * Public rooms: Fix the infinite loading of the public rooms list after logging out & in. + * iOS should have 'Send a message (encrypted)' in placeholder (#1231). + * Fix dangling in the memory CallViewController, thanks to @morozkin (#1248). + * Fix crash in MediaPickerViewController (#1252). + * Fix crash in global search (https://github.com/matrix-org/riot-ios-rageshakes#32). + * Fix crash in [MXKContactManager localContactsSplitByContactMethod] (https://github.com/matrix-org/riot-ios-rageshakes#36). + * Fix App crashes on [AvatarGenerator imageFromText:withBackgroundColor:] (#657). + Changes in 0.3.13 (2017-03-23) =============================================== diff --git a/Podfile b/Podfile index ea5854710..60bb76b2d 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, "7.0" +platform :ios, "8.0" source 'https://github.com/CocoaPods/Specs.git' @@ -8,7 +8,7 @@ target "Riot" do # Different flavours of pods to MatrixKit # The tagged version on which this version of Riot has been built -pod 'MatrixKit', '0.4.11' +pod 'MatrixKit', '0.5.0' # The lastest release available on the CocoaPods repository #pod 'MatrixKit' @@ -22,17 +22,22 @@ pod 'MatrixKit', '0.4.11' #pod 'MatrixKit', :path => '../matrix-ios-kit/MatrixKit.podspec' #pod 'MatrixSDK', :path => '../matrix-ios-sdk/MatrixSDK.podspec' -pod 'GBDeviceInfo', '~> 4.2.2' +pod 'GBDeviceInfo', '~> 4.3.0' pod 'GoogleAnalytics' # The Google WebRTC stack -pod 'WebRTC', '56.10.15101' +pod 'WebRTC', '58.17.16937' # OLMKit for crypto pod 'OLMKit' #pod 'OLMKit', :path => '../olm/OLMKit.podspec' -pod 'Realm', '~> 2.1.1' +pod 'Realm', '~> 2.8.1' + +# Remove warnings from "bad" pods +pod 'OLMKit', :inhibit_warnings => true +pod 'cmark', :inhibit_warnings => true +pod 'DTCoreText', :inhibit_warnings => true end diff --git a/Podfile.lock b/Podfile.lock index 06b3e5fd1..52fc909d5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -32,53 +32,58 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.12): - DTFoundation/Core - - GBDeviceInfo (4.2.2): - - GBDeviceInfo/Core (= 4.2.2) - - GBDeviceInfo/Core (4.2.2) + - GBDeviceInfo (4.3.0): + - GBDeviceInfo/Core (= 4.3.0) + - GBDeviceInfo/Core (4.3.0) - GoogleAnalytics (3.17.0) + - GZIP (1.1.1) - HPGrowingTextView (1.1) - - libPhoneNumber-iOS (0.8.17) - - MatrixKit (0.4.11): + - libPhoneNumber-iOS (0.9.10) + - MatrixKit (0.5.0): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.17) - HPGrowingTextView (~> 1.1) - - libPhoneNumber-iOS (~> 0.8.14) - - MatrixSDK (= 0.7.11) - - MatrixSDK (0.7.11): + - libPhoneNumber-iOS (~> 0.9.10) + - MatrixSDK (= 0.8.0) + - MatrixSDK (0.8.0): - AFNetworking (~> 3.1.0) + - GZIP (~> 1.1.1) - OLMKit (2.2.2): - OLMKit/olmc (= 2.2.2) - OLMKit/olmcpp (= 2.2.2) - OLMKit/olmc (2.2.2) - OLMKit/olmcpp (2.2.2) - - Realm (2.1.2): - - Realm/Headers (= 2.1.2) - - Realm/Headers (2.1.2) - - WebRTC (56.10.15101) + - Realm (2.8.1): + - Realm/Headers (= 2.8.1) + - Realm/Headers (2.8.1) + - WebRTC (58.17.16937) DEPENDENCIES: - - GBDeviceInfo (~> 4.2.2) + - cmark + - DTCoreText + - GBDeviceInfo (~> 4.3.0) - GoogleAnalytics - - MatrixKit (= 0.4.11) + - MatrixKit (= 0.5.0) - OLMKit - - Realm (~> 2.1.1) - - WebRTC (= 56.10.15101) + - Realm (~> 2.8.1) + - WebRTC (= 58.17.16937) SPEC CHECKSUMS: AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67 cmark: ec0275215b504780287b6fca360224e384368af8 DTCoreText: 51904f2374af443e0d270d6fdc76035ab6f9ef8a DTFoundation: 26a164ef510877387906fb92bff524a792db4bf8 - GBDeviceInfo: 0a6e2fc04989ce248572bb988f1a764102eb0e5d + GBDeviceInfo: caae36532afcc209b51ac62bba547aadab9e88f2 GoogleAnalytics: f42cc53a87a51fe94334821868d9c8481ff47a7b + GZIP: f8beb59597f651e6970a45b816508a9c6d700b77 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 - libPhoneNumber-iOS: 9f083847f8cb9b81064cff2ed2c98cbf18d9f9f2 - MatrixKit: 72a7d62f3484a44ce7e9f9f850e4f2fd64088007 - MatrixSDK: 7508abee816785312d658dc7569ea63074025c27 + libPhoneNumber-iOS: f721ae4d5854bce60934f9fb9b0b28e8e68913cb + MatrixKit: 3783ee9b8b049a05a5856c66f749319ec1592161 + MatrixSDK: 6d1bf7ffc4792622da5441b4ddc6d5c02a0675b3 OLMKit: b9d8c0ffee9ea8c45bc0aaa9afb47f93fba7efbd - Realm: efe855f4d977c8ce5a82d3116d9f1ff155a6550c - WebRTC: 116e2a81290a8551d67dd2e471b94803a5bba813 + Realm: 2627602ad6818451f0cb8c2a6e072f7f10a5f360 + WebRTC: 1e9a85bf75509eec44be6478c64e9de65ac82332 -PODFILE CHECKSUM: 9d16cdea9e05545d9a0362c174a348ff9d9450ef +PODFILE CHECKSUM: 24c0a0406f24e3ba56478f5e9322cda986a7c199 -COCOAPODS: 1.2.0 +COCOAPODS: 1.2.1 diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 752c21b6f..c767c6dd3 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -7,9 +7,31 @@ objects = { /* Begin PBXBuildFile section */ + 3205ED7D1E976C8A003D65FA /* DirectoryServerPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3205ED7C1E976C8A003D65FA /* DirectoryServerPickerViewController.m */; }; + 3205ED841E97725E003D65FA /* DirectoryServerTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3205ED821E97725E003D65FA /* DirectoryServerTableViewCell.m */; }; + 3205ED851E97725E003D65FA /* DirectoryServerTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3205ED831E97725E003D65FA /* DirectoryServerTableViewCell.xib */; }; 325072141E8C0AC900A084B6 /* LaunchScreenLogo.png in Resources */ = {isa = PBXBuildFile; fileRef = 325072131E8C0AC900A084B6 /* LaunchScreenLogo.png */; }; 325E1C151E8D03950018D91E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 325E1C131E8D03950018D91E /* LaunchScreen.storyboard */; }; + 32D392181EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 32D392161EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.m */; }; + 32D392191EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32D392171EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib */; }; + 32FD0A3D1EB0CD9B0072B066 /* BugReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */; }; + 32FD0A3E1EB0CD9B0072B066 /* BugReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */; }; E2EAC1A4FBD6FE5228584591 /* libPods-Riot.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D8737F782E108CFD6908691 /* libPods-Riot.a */; }; + F02C1A861E8EB04C0045A404 /* PeopleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F02C1A841E8EB04C0045A404 /* PeopleViewController.m */; }; + F05BD79E1E7AEBF800C69941 /* UnifiedSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F05BD79D1E7AEBF800C69941 /* UnifiedSearchViewController.m */; }; + F05BD7A11E7C0E4500C69941 /* MasterTabBarController.m in Sources */ = {isa = PBXBuildFile; fileRef = F05BD7A01E7C0E4500C69941 /* MasterTabBarController.m */; }; + F0614A0D1EDDCCE700F5DC9A /* jump_to_unread.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A0A1EDDCCE700F5DC9A /* jump_to_unread.png */; }; + F0614A0E1EDDCCE700F5DC9A /* jump_to_unread@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A0B1EDDCCE700F5DC9A /* jump_to_unread@2x.png */; }; + F0614A0F1EDDCCE700F5DC9A /* jump_to_unread@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A0C1EDDCCE700F5DC9A /* jump_to_unread@3x.png */; }; + F0614A131EDEE65000F5DC9A /* cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A101EDEE65000F5DC9A /* cancel.png */; }; + F0614A141EDEE65000F5DC9A /* cancel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A111EDEE65000F5DC9A /* cancel@2x.png */; }; + F0614A151EDEE65000F5DC9A /* cancel@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0614A121EDEE65000F5DC9A /* cancel@3x.png */; }; + F06CDD691EF01E3900870B75 /* RoomEmptyBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F06CDD671EF01E3900870B75 /* RoomEmptyBubbleCell.m */; }; + F06CDD6A1EF01E3900870B75 /* RoomEmptyBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F06CDD681EF01E3900870B75 /* RoomEmptyBubbleCell.xib */; }; + F075BED61EBB169C00A7B68A /* RoomCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F075BED41EBB169C00A7B68A /* RoomCollectionViewCell.m */; }; + F075BED71EBB169C00A7B68A /* RoomCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F075BED51EBB169C00A7B68A /* RoomCollectionViewCell.xib */; }; + F075BEDB1EBB26F100A7B68A /* TableViewCellWithCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = F075BED91EBB26F100A7B68A /* TableViewCellWithCollectionView.m */; }; + F075BEDC1EBB26F100A7B68A /* TableViewCellWithCollectionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F075BEDA1EBB26F100A7B68A /* TableViewCellWithCollectionView.xib */; }; F083BD1D1E7009ED00A9B29C /* RageShakeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BB0B1E7009EC00A9B29C /* RageShakeManager.m */; }; F083BD1E1E7009ED00A9B29C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BB0D1E7009EC00A9B29C /* AppDelegate.m */; }; F083BD1F1E7009ED00A9B29C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = F083BB0F1E7009EC00A9B29C /* InfoPlist.strings */; }; @@ -400,6 +422,38 @@ F083BEA21E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BD1B1E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.m */; }; F083BEA31E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.xib in Resources */ = {isa = PBXBuildFile; fileRef = F083BD1C1E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.xib */; }; F083BEA51E70356E00A9B29C /* RiotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BB041E7005FD00A9B29C /* RiotTests.m */; }; + F0D869EB1EC455A100BB0A2B /* create_direct_chat.png in Resources */ = {isa = PBXBuildFile; fileRef = F0D869E81EC455A100BB0A2B /* create_direct_chat.png */; }; + F0D869EC1EC455A100BB0A2B /* create_direct_chat@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0D869E91EC455A100BB0A2B /* create_direct_chat@2x.png */; }; + F0D869ED1EC455A100BB0A2B /* create_direct_chat@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0D869EA1EC455A100BB0A2B /* create_direct_chat@3x.png */; }; + F0E059FD1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E059FC1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.m */; }; + F0E05A021E963103004B83FB /* FavouritesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E059FF1E963103004B83FB /* FavouritesViewController.m */; }; + F0E05A031E963103004B83FB /* RoomsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E05A011E963103004B83FB /* RoomsViewController.m */; }; + F0E05A061E9682E9004B83FB /* ContactsDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E05A051E9682E9004B83FB /* ContactsDataSource.m */; }; + F0E05A0B1E9CCEBF004B83FB /* RecentsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A0A1E9CCEBF004B83FB /* RecentsViewController.xib */; }; + F0E05A301EA0F9EB004B83FB /* tab_favourites_selected.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A181EA0F9EB004B83FB /* tab_favourites_selected.png */; }; + F0E05A311EA0F9EB004B83FB /* tab_favourites_selected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A191EA0F9EB004B83FB /* tab_favourites_selected@2x.png */; }; + F0E05A321EA0F9EB004B83FB /* tab_favourites_selected@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A1A1EA0F9EB004B83FB /* tab_favourites_selected@3x.png */; }; + F0E05A331EA0F9EB004B83FB /* tab_favourites.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A1B1EA0F9EB004B83FB /* tab_favourites.png */; }; + F0E05A341EA0F9EB004B83FB /* tab_favourites@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A1C1EA0F9EB004B83FB /* tab_favourites@2x.png */; }; + F0E05A351EA0F9EB004B83FB /* tab_favourites@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A1D1EA0F9EB004B83FB /* tab_favourites@3x.png */; }; + F0E05A361EA0F9EB004B83FB /* tab_home_selected.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A1E1EA0F9EB004B83FB /* tab_home_selected.png */; }; + F0E05A371EA0F9EB004B83FB /* tab_home_selected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A1F1EA0F9EB004B83FB /* tab_home_selected@2x.png */; }; + F0E05A381EA0F9EB004B83FB /* tab_home_selected@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A201EA0F9EB004B83FB /* tab_home_selected@3x.png */; }; + F0E05A391EA0F9EB004B83FB /* tab_home.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A211EA0F9EB004B83FB /* tab_home.png */; }; + F0E05A3A1EA0F9EB004B83FB /* tab_home@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A221EA0F9EB004B83FB /* tab_home@2x.png */; }; + F0E05A3B1EA0F9EB004B83FB /* tab_home@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A231EA0F9EB004B83FB /* tab_home@3x.png */; }; + F0E05A3C1EA0F9EB004B83FB /* tab_people_selected.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A241EA0F9EB004B83FB /* tab_people_selected.png */; }; + F0E05A3D1EA0F9EB004B83FB /* tab_people_selected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A251EA0F9EB004B83FB /* tab_people_selected@2x.png */; }; + F0E05A3E1EA0F9EB004B83FB /* tab_people_selected@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A261EA0F9EB004B83FB /* tab_people_selected@3x.png */; }; + F0E05A3F1EA0F9EB004B83FB /* tab_people.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A271EA0F9EB004B83FB /* tab_people.png */; }; + F0E05A401EA0F9EB004B83FB /* tab_people@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A281EA0F9EB004B83FB /* tab_people@2x.png */; }; + F0E05A411EA0F9EB004B83FB /* tab_people@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A291EA0F9EB004B83FB /* tab_people@3x.png */; }; + F0E05A421EA0F9EB004B83FB /* tab_rooms_selected.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A2A1EA0F9EB004B83FB /* tab_rooms_selected.png */; }; + F0E05A431EA0F9EB004B83FB /* tab_rooms_selected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A2B1EA0F9EB004B83FB /* tab_rooms_selected@2x.png */; }; + F0E05A441EA0F9EB004B83FB /* tab_rooms_selected@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A2C1EA0F9EB004B83FB /* tab_rooms_selected@3x.png */; }; + F0E05A451EA0F9EB004B83FB /* tab_rooms.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A2D1EA0F9EB004B83FB /* tab_rooms.png */; }; + F0E05A461EA0F9EB004B83FB /* tab_rooms@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A2E1EA0F9EB004B83FB /* tab_rooms@2x.png */; }; + F0E05A471EA0F9EB004B83FB /* tab_rooms@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0E05A2F1EA0F9EB004B83FB /* tab_rooms@3x.png */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -414,9 +468,41 @@ /* Begin PBXFileReference section */ 1129C74A281B080432B1A1A1 /* Pods-Riot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.debug.xcconfig"; sourceTree = ""; }; + 3205ED7B1E976C8A003D65FA /* DirectoryServerPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryServerPickerViewController.h; sourceTree = ""; }; + 3205ED7C1E976C8A003D65FA /* DirectoryServerPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryServerPickerViewController.m; sourceTree = ""; }; + 3205ED811E97725E003D65FA /* DirectoryServerTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryServerTableViewCell.h; sourceTree = ""; }; + 3205ED821E97725E003D65FA /* DirectoryServerTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryServerTableViewCell.m; sourceTree = ""; }; + 3205ED831E97725E003D65FA /* DirectoryServerTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DirectoryServerTableViewCell.xib; sourceTree = ""; }; 325072131E8C0AC900A084B6 /* LaunchScreenLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = LaunchScreenLogo.png; path = Assets/Images/LaunchScreenLogo.png; sourceTree = ""; }; 325E1C141E8D03950018D91E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 32D392151EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryServerDetailTableViewCell.h; sourceTree = ""; }; + 32D392161EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryServerDetailTableViewCell.m; sourceTree = ""; }; + 32D392171EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DirectoryServerDetailTableViewCell.xib; sourceTree = ""; }; + 32FD0A3A1EB0CD9B0072B066 /* BugReportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugReportViewController.h; sourceTree = ""; }; + 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugReportViewController.m; sourceTree = ""; }; + 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BugReportViewController.xib; sourceTree = ""; }; 7D8737F782E108CFD6908691 /* libPods-Riot.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Riot.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F02C1A831E8EB04C0045A404 /* PeopleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PeopleViewController.h; sourceTree = ""; }; + F02C1A841E8EB04C0045A404 /* PeopleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PeopleViewController.m; sourceTree = ""; }; + F05BD79C1E7AEBF800C69941 /* UnifiedSearchViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UnifiedSearchViewController.h; sourceTree = ""; }; + F05BD79D1E7AEBF800C69941 /* UnifiedSearchViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UnifiedSearchViewController.m; sourceTree = ""; }; + F05BD79F1E7C0E4500C69941 /* MasterTabBarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MasterTabBarController.h; sourceTree = ""; }; + F05BD7A01E7C0E4500C69941 /* MasterTabBarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MasterTabBarController.m; sourceTree = ""; }; + F0614A0A1EDDCCE700F5DC9A /* jump_to_unread.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = jump_to_unread.png; sourceTree = ""; }; + F0614A0B1EDDCCE700F5DC9A /* jump_to_unread@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "jump_to_unread@2x.png"; sourceTree = ""; }; + F0614A0C1EDDCCE700F5DC9A /* jump_to_unread@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "jump_to_unread@3x.png"; sourceTree = ""; }; + F0614A101EDEE65000F5DC9A /* cancel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cancel.png; sourceTree = ""; }; + F0614A111EDEE65000F5DC9A /* cancel@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "cancel@2x.png"; sourceTree = ""; }; + F0614A121EDEE65000F5DC9A /* cancel@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "cancel@3x.png"; sourceTree = ""; }; + F06CDD661EF01E3900870B75 /* RoomEmptyBubbleCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomEmptyBubbleCell.h; sourceTree = ""; }; + F06CDD671EF01E3900870B75 /* RoomEmptyBubbleCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomEmptyBubbleCell.m; sourceTree = ""; }; + F06CDD681EF01E3900870B75 /* RoomEmptyBubbleCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomEmptyBubbleCell.xib; sourceTree = ""; }; + F075BED31EBB169C00A7B68A /* RoomCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomCollectionViewCell.h; sourceTree = ""; }; + F075BED41EBB169C00A7B68A /* RoomCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomCollectionViewCell.m; sourceTree = ""; }; + F075BED51EBB169C00A7B68A /* RoomCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomCollectionViewCell.xib; sourceTree = ""; }; + F075BED81EBB26F100A7B68A /* TableViewCellWithCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TableViewCellWithCollectionView.h; sourceTree = ""; }; + F075BED91EBB26F100A7B68A /* TableViewCellWithCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TableViewCellWithCollectionView.m; sourceTree = ""; }; + F075BEDA1EBB26F100A7B68A /* TableViewCellWithCollectionView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TableViewCellWithCollectionView.xib; sourceTree = ""; }; F083BB031E7005FD00A9B29C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F083BB041E7005FD00A9B29C /* RiotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RiotTests.m; sourceTree = ""; }; F083BB0A1E7009EC00A9B29C /* RageShakeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RageShakeManager.h; sourceTree = ""; }; @@ -920,6 +1006,42 @@ F083BD1C1E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TableViewCellWithPhoneNumberTextField.xib; sourceTree = ""; }; F094A9A21B78D8F000B1FBBF /* Riot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Riot.app; sourceTree = BUILT_PRODUCTS_DIR; }; F094A9BE1B78D8F000B1FBBF /* RiotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RiotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F0D869E81EC455A100BB0A2B /* create_direct_chat.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = create_direct_chat.png; sourceTree = ""; }; + F0D869E91EC455A100BB0A2B /* create_direct_chat@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "create_direct_chat@2x.png"; sourceTree = ""; }; + F0D869EA1EC455A100BB0A2B /* create_direct_chat@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "create_direct_chat@3x.png"; sourceTree = ""; }; + F0E059FB1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UnifiedSearchRecentsDataSource.h; sourceTree = ""; }; + F0E059FC1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UnifiedSearchRecentsDataSource.m; sourceTree = ""; }; + F0E059FE1E963103004B83FB /* FavouritesViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FavouritesViewController.h; sourceTree = ""; }; + F0E059FF1E963103004B83FB /* FavouritesViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FavouritesViewController.m; sourceTree = ""; }; + F0E05A001E963103004B83FB /* RoomsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomsViewController.h; sourceTree = ""; }; + F0E05A011E963103004B83FB /* RoomsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomsViewController.m; sourceTree = ""; }; + F0E05A041E9682E9004B83FB /* ContactsDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsDataSource.h; sourceTree = ""; }; + F0E05A051E9682E9004B83FB /* ContactsDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactsDataSource.m; sourceTree = ""; }; + F0E05A0A1E9CCEBF004B83FB /* RecentsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RecentsViewController.xib; sourceTree = ""; }; + F0E05A181EA0F9EB004B83FB /* tab_favourites_selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_favourites_selected.png; sourceTree = ""; }; + F0E05A191EA0F9EB004B83FB /* tab_favourites_selected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_favourites_selected@2x.png"; sourceTree = ""; }; + F0E05A1A1EA0F9EB004B83FB /* tab_favourites_selected@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_favourites_selected@3x.png"; sourceTree = ""; }; + F0E05A1B1EA0F9EB004B83FB /* tab_favourites.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_favourites.png; sourceTree = ""; }; + F0E05A1C1EA0F9EB004B83FB /* tab_favourites@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_favourites@2x.png"; sourceTree = ""; }; + F0E05A1D1EA0F9EB004B83FB /* tab_favourites@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_favourites@3x.png"; sourceTree = ""; }; + F0E05A1E1EA0F9EB004B83FB /* tab_home_selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_home_selected.png; sourceTree = ""; }; + F0E05A1F1EA0F9EB004B83FB /* tab_home_selected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_home_selected@2x.png"; sourceTree = ""; }; + F0E05A201EA0F9EB004B83FB /* tab_home_selected@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_home_selected@3x.png"; sourceTree = ""; }; + F0E05A211EA0F9EB004B83FB /* tab_home.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_home.png; sourceTree = ""; }; + F0E05A221EA0F9EB004B83FB /* tab_home@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_home@2x.png"; sourceTree = ""; }; + F0E05A231EA0F9EB004B83FB /* tab_home@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_home@3x.png"; sourceTree = ""; }; + F0E05A241EA0F9EB004B83FB /* tab_people_selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_people_selected.png; sourceTree = ""; }; + F0E05A251EA0F9EB004B83FB /* tab_people_selected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_people_selected@2x.png"; sourceTree = ""; }; + F0E05A261EA0F9EB004B83FB /* tab_people_selected@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_people_selected@3x.png"; sourceTree = ""; }; + F0E05A271EA0F9EB004B83FB /* tab_people.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_people.png; sourceTree = ""; }; + F0E05A281EA0F9EB004B83FB /* tab_people@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_people@2x.png"; sourceTree = ""; }; + F0E05A291EA0F9EB004B83FB /* tab_people@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_people@3x.png"; sourceTree = ""; }; + F0E05A2A1EA0F9EB004B83FB /* tab_rooms_selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_rooms_selected.png; sourceTree = ""; }; + F0E05A2B1EA0F9EB004B83FB /* tab_rooms_selected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_rooms_selected@2x.png"; sourceTree = ""; }; + F0E05A2C1EA0F9EB004B83FB /* tab_rooms_selected@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_rooms_selected@3x.png"; sourceTree = ""; }; + F0E05A2D1EA0F9EB004B83FB /* tab_rooms.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tab_rooms.png; sourceTree = ""; }; + F0E05A2E1EA0F9EB004B83FB /* tab_rooms@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_rooms@2x.png"; sourceTree = ""; }; + F0E05A2F1EA0F9EB004B83FB /* tab_rooms@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tab_rooms@3x.png"; sourceTree = ""; }; F9D678EF54918C036FDEDBF9 /* Pods-Riot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.release.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -942,6 +1064,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3205ED801E97725E003D65FA /* Directory */ = { + isa = PBXGroup; + children = ( + 32D392151EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.h */, + 32D392161EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.m */, + 32D392171EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib */, + 3205ED811E97725E003D65FA /* DirectoryServerTableViewCell.h */, + 3205ED821E97725E003D65FA /* DirectoryServerTableViewCell.m */, + 3205ED831E97725E003D65FA /* DirectoryServerTableViewCell.xib */, + ); + path = Directory; + sourceTree = ""; + }; 6567B0BBF3C05D7F7A7F79CC /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1021,6 +1156,39 @@ F083BB151E7009EC00A9B29C /* Images */ = { isa = PBXGroup; children = ( + F0614A101EDEE65000F5DC9A /* cancel.png */, + F0614A111EDEE65000F5DC9A /* cancel@2x.png */, + F0614A121EDEE65000F5DC9A /* cancel@3x.png */, + F0614A0A1EDDCCE700F5DC9A /* jump_to_unread.png */, + F0614A0B1EDDCCE700F5DC9A /* jump_to_unread@2x.png */, + F0614A0C1EDDCCE700F5DC9A /* jump_to_unread@3x.png */, + F0D869E81EC455A100BB0A2B /* create_direct_chat.png */, + F0D869E91EC455A100BB0A2B /* create_direct_chat@2x.png */, + F0D869EA1EC455A100BB0A2B /* create_direct_chat@3x.png */, + F0E05A181EA0F9EB004B83FB /* tab_favourites_selected.png */, + F0E05A191EA0F9EB004B83FB /* tab_favourites_selected@2x.png */, + F0E05A1A1EA0F9EB004B83FB /* tab_favourites_selected@3x.png */, + F0E05A1B1EA0F9EB004B83FB /* tab_favourites.png */, + F0E05A1C1EA0F9EB004B83FB /* tab_favourites@2x.png */, + F0E05A1D1EA0F9EB004B83FB /* tab_favourites@3x.png */, + F0E05A1E1EA0F9EB004B83FB /* tab_home_selected.png */, + F0E05A1F1EA0F9EB004B83FB /* tab_home_selected@2x.png */, + F0E05A201EA0F9EB004B83FB /* tab_home_selected@3x.png */, + F0E05A211EA0F9EB004B83FB /* tab_home.png */, + F0E05A221EA0F9EB004B83FB /* tab_home@2x.png */, + F0E05A231EA0F9EB004B83FB /* tab_home@3x.png */, + F0E05A241EA0F9EB004B83FB /* tab_people_selected.png */, + F0E05A251EA0F9EB004B83FB /* tab_people_selected@2x.png */, + F0E05A261EA0F9EB004B83FB /* tab_people_selected@3x.png */, + F0E05A271EA0F9EB004B83FB /* tab_people.png */, + F0E05A281EA0F9EB004B83FB /* tab_people@2x.png */, + F0E05A291EA0F9EB004B83FB /* tab_people@3x.png */, + F0E05A2A1EA0F9EB004B83FB /* tab_rooms_selected.png */, + F0E05A2B1EA0F9EB004B83FB /* tab_rooms_selected@2x.png */, + F0E05A2C1EA0F9EB004B83FB /* tab_rooms_selected@3x.png */, + F0E05A2D1EA0F9EB004B83FB /* tab_rooms.png */, + F0E05A2E1EA0F9EB004B83FB /* tab_rooms@2x.png */, + F0E05A2F1EA0F9EB004B83FB /* tab_rooms@3x.png */, F083BB161E7009EC00A9B29C /* add_participant.png */, F083BB171E7009EC00A9B29C /* add_participant@2x.png */, F083BB181E7009EC00A9B29C /* add_participant@3x.png */, @@ -1262,6 +1430,8 @@ F083BBF31E7009EC00A9B29C /* Contact */ = { isa = PBXGroup; children = ( + F0E05A041E9682E9004B83FB /* ContactsDataSource.h */, + F0E05A051E9682E9004B83FB /* ContactsDataSource.m */, F083BBF41E7009EC00A9B29C /* Contact.h */, F083BBF51E7009EC00A9B29C /* Contact.m */, ); @@ -1296,6 +1466,8 @@ F083BC071E7009EC00A9B29C /* RecentCellData.m */, F083BC081E7009EC00A9B29C /* RecentsDataSource.h */, F083BC091E7009EC00A9B29C /* RecentsDataSource.m */, + F0E059FB1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.h */, + F0E059FC1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.m */, ); path = RoomList; sourceTree = ""; @@ -1327,11 +1499,24 @@ F083BC191E7009EC00A9B29C /* ViewController */ = { isa = PBXGroup; children = ( + F0E059FE1E963103004B83FB /* FavouritesViewController.h */, + F0E059FF1E963103004B83FB /* FavouritesViewController.m */, + F0E05A001E963103004B83FB /* RoomsViewController.h */, + F0E05A011E963103004B83FB /* RoomsViewController.m */, + F02C1A831E8EB04C0045A404 /* PeopleViewController.h */, + F02C1A841E8EB04C0045A404 /* PeopleViewController.m */, + F05BD79F1E7C0E4500C69941 /* MasterTabBarController.h */, + F05BD7A01E7C0E4500C69941 /* MasterTabBarController.m */, + F05BD79C1E7AEBF800C69941 /* UnifiedSearchViewController.h */, + F05BD79D1E7AEBF800C69941 /* UnifiedSearchViewController.m */, F083BC1A1E7009EC00A9B29C /* AttachmentsViewController.h */, F083BC1B1E7009EC00A9B29C /* AttachmentsViewController.m */, F083BC1C1E7009EC00A9B29C /* AuthenticationViewController.h */, F083BC1D1E7009EC00A9B29C /* AuthenticationViewController.m */, F083BC1E1E7009EC00A9B29C /* AuthenticationViewController.xib */, + 32FD0A3A1EB0CD9B0072B066 /* BugReportViewController.h */, + 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */, + 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */, F083BC1F1E7009EC00A9B29C /* CallViewController.h */, F083BC201E7009EC00A9B29C /* CallViewController.m */, F083BC211E7009EC00A9B29C /* CallViewController.xib */, @@ -1345,6 +1530,8 @@ F083BC291E7009EC00A9B29C /* CountryPickerViewController.m */, F083BC2A1E7009EC00A9B29C /* DirectoryViewController.h */, F083BC2B1E7009EC00A9B29C /* DirectoryViewController.m */, + 3205ED7B1E976C8A003D65FA /* DirectoryServerPickerViewController.h */, + 3205ED7C1E976C8A003D65FA /* DirectoryServerPickerViewController.m */, F083BC2C1E7009EC00A9B29C /* HomeFilesSearchViewController.h */, F083BC2D1E7009EC00A9B29C /* HomeFilesSearchViewController.m */, F083BC2E1E7009EC00A9B29C /* HomeMessagesSearchViewController.h */, @@ -1359,6 +1546,7 @@ F083BC371E7009EC00A9B29C /* MediaPickerViewController.xib */, F083BC381E7009EC00A9B29C /* RecentsViewController.h */, F083BC391E7009EC00A9B29C /* RecentsViewController.m */, + F0E05A0A1E9CCEBF004B83FB /* RecentsViewController.xib */, F083BC3A1E7009EC00A9B29C /* RoomFilesSearchViewController.h */, F083BC3B1E7009EC00A9B29C /* RoomFilesSearchViewController.m */, F083BC3C1E7009EC00A9B29C /* RoomFilesViewController.h */, @@ -1398,6 +1586,7 @@ F083BC581E7009EC00A9B29C /* Authentication */, F083BC5F1E7009EC00A9B29C /* Contact */, F083BC631E7009EC00A9B29C /* Device */, + 3205ED801E97725E003D65FA /* Directory */, F083BC691E7009EC00A9B29C /* EncryptionInfoView */, F083BC6C1E7009EC00A9B29C /* MediaAlbum */, F083BC701E7009EC00A9B29C /* RoomActivitiesView */, @@ -1480,6 +1669,9 @@ isa = PBXGroup; children = ( F083BC751E7009EC00A9B29C /* Encryption */, + F06CDD661EF01E3900870B75 /* RoomEmptyBubbleCell.h */, + F06CDD671EF01E3900870B75 /* RoomEmptyBubbleCell.m */, + F06CDD681EF01E3900870B75 /* RoomEmptyBubbleCell.xib */, F083BCA81E7009EC00A9B29C /* RoomIncomingAttachmentBubbleCell.h */, F083BCA91E7009EC00A9B29C /* RoomIncomingAttachmentBubbleCell.m */, F083BCAA1E7009EC00A9B29C /* RoomIncomingAttachmentBubbleCell.xib */, @@ -1602,6 +1794,9 @@ F083BCDC1E7009EC00A9B29C /* RoomList */ = { isa = PBXGroup; children = ( + F075BED31EBB169C00A7B68A /* RoomCollectionViewCell.h */, + F075BED41EBB169C00A7B68A /* RoomCollectionViewCell.m */, + F075BED51EBB169C00A7B68A /* RoomCollectionViewCell.xib */, F083BCDD1E7009EC00A9B29C /* DirectoryRecentTableViewCell.h */, F083BCDE1E7009EC00A9B29C /* DirectoryRecentTableViewCell.m */, F083BCDF1E7009EC00A9B29C /* DirectoryRecentTableViewCell.xib */, @@ -1675,6 +1870,9 @@ F083BD0D1E7009ED00A9B29C /* TableViewCell */ = { isa = PBXGroup; children = ( + F075BED81EBB26F100A7B68A /* TableViewCellWithCollectionView.h */, + F075BED91EBB26F100A7B68A /* TableViewCellWithCollectionView.m */, + F075BEDA1EBB26F100A7B68A /* TableViewCellWithCollectionView.xib */, F083BD0E1E7009ED00A9B29C /* TableViewCellWithButton.h */, F083BD0F1E7009ED00A9B29C /* TableViewCellWithButton.m */, F083BD101E7009ED00A9B29C /* TableViewCellWithButton.xib */, @@ -1842,8 +2040,10 @@ F083BE261E7009ED00A9B29C /* SegmentedViewController.xib in Resources */, F083BE161E7009ED00A9B29C /* MediaAlbumContentViewController.xib in Resources */, F083BD441E7009ED00A9B29C /* call_speaker_on_icon@3x.png in Resources */, + F0614A0F1EDDCCE700F5DC9A /* jump_to_unread@3x.png in Resources */, F083BE511E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgBubbleCell.xib in Resources */, F083BDAE1E7009ED00A9B29C /* plus_icon@2x.png in Resources */, + F0E05A401EA0F9EB004B83FB /* tab_people@2x.png in Resources */, F083BD5A1E7009ED00A9B29C /* chevron@2x.png in Resources */, F083BDB81E7009ED00A9B29C /* remove_icon@3x.png in Resources */, F083BDA91E7009ED00A9B29C /* notificationsOff@3x.png in Resources */, @@ -1860,9 +2060,12 @@ F083BDA61E7009ED00A9B29C /* notifications@3x.png in Resources */, F083BD771E7009ED00A9B29C /* e2e_warning.png in Resources */, F083BDDB1E7009ED00A9B29C /* typing@2x.png in Resources */, + F075BEDC1EBB26F100A7B68A /* TableViewCellWithCollectionView.xib in Resources */, F083BDE71E7009ED00A9B29C /* callend.mp3 in Resources */, F083BD231E7009ED00A9B29C /* add_participant@2x.png in Resources */, F083BDA51E7009ED00A9B29C /* notifications@2x.png in Resources */, + F0E05A3F1EA0F9EB004B83FB /* tab_people.png in Resources */, + F0E05A351EA0F9EB004B83FB /* tab_favourites@3x.png in Resources */, F083BE611E7009ED00A9B29C /* RoomIncomingTextMsgBubbleCell.xib in Resources */, F083BE9D1E7009ED00A9B29C /* TableViewCellWithCheckBoxAndLabel.xib in Resources */, F083BDC41E7009ED00A9B29C /* scrollup@3x.png in Resources */, @@ -1894,13 +2097,18 @@ F083BD7B1E7009ED00A9B29C /* edit_icon@2x.png in Resources */, F083BDAF1E7009ED00A9B29C /* plus_icon@3x.png in Resources */, F083BDD91E7009ED00A9B29C /* start_chat@3x.png in Resources */, + F0614A141EDEE65000F5DC9A /* cancel@2x.png in Resources */, F083BE411E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgBubbleCell.xib in Resources */, + F0E05A0B1E9CCEBF004B83FB /* RecentsViewController.xib in Resources */, F083BD5F1E7009ED00A9B29C /* details_icon.png in Resources */, F083BE241E7009ED00A9B29C /* RoomViewController.xib in Resources */, F083BE7B1E7009ED00A9B29C /* RoomInputToolbarView.xib in Resources */, + F0E05A3D1EA0F9EB004B83FB /* tab_people_selected@2x.png in Resources */, + F0E05A451EA0F9EB004B83FB /* tab_rooms.png in Resources */, F083BE751E7009ED00A9B29C /* RoomOutgoingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, F083BD261E7009ED00A9B29C /* admin_icon@2x.png in Resources */, 325072141E8C0AC900A084B6 /* LaunchScreenLogo.png in Resources */, + 3205ED851E97725E003D65FA /* DirectoryServerTableViewCell.xib in Resources */, F083BD761E7009ED00A9B29C /* e2e_verified@3x.png in Resources */, F083BE181E7009ED00A9B29C /* MediaPickerViewController.xib in Resources */, F083BDCE1E7009ED00A9B29C /* selection_untick.png in Resources */, @@ -1916,6 +2124,7 @@ F083BD3C1E7009ED00A9B29C /* call_hangup_icon.png in Resources */, F083BE6D1E7009ED00A9B29C /* RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, F083BE9F1E7009ED00A9B29C /* TableViewCellWithCheckBoxes.xib in Resources */, + F0E05A421EA0F9EB004B83FB /* tab_rooms_selected.png in Resources */, F083BE4F1E7009ED00A9B29C /* RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */, F083BD241E7009ED00A9B29C /* add_participant@3x.png in Resources */, F083BD891E7009ED00A9B29C /* file_music_icon.png in Resources */, @@ -1935,10 +2144,14 @@ F083BE011E7009ED00A9B29C /* third_party_licenses.html in Resources */, F083BD751E7009ED00A9B29C /* e2e_verified@2x.png in Resources */, F083BDCD1E7009ED00A9B29C /* selection_tick@3x.png in Resources */, + F0E05A311EA0F9EB004B83FB /* tab_favourites_selected@2x.png in Resources */, F083BD741E7009ED00A9B29C /* e2e_verified.png in Resources */, F083BD2A1E7009ED00A9B29C /* animatedLogo-2.png in Resources */, + F0E05A471EA0F9EB004B83FB /* tab_rooms@3x.png in Resources */, F083BD8E1E7009ED00A9B29C /* file_photo_icon@3x.png in Resources */, + F0E05A361EA0F9EB004B83FB /* tab_home_selected.png in Resources */, F083BDE41E7009ED00A9B29C /* voice_call_icon@2x.png in Resources */, + F0E05A411EA0F9EB004B83FB /* tab_people@3x.png in Resources */, F083BD8C1E7009ED00A9B29C /* file_photo_icon.png in Resources */, F083BE911E7009ED00A9B29C /* RoomTitleView.xib in Resources */, F083BE4B1E7009ED00A9B29C /* RoomOutgoingEncryptedAttachmentBubbleCell.xib in Resources */, @@ -1960,20 +2173,26 @@ F083BD5D1E7009ED00A9B29C /* create_room@2x.png in Resources */, F083BD921E7009ED00A9B29C /* group.png in Resources */, F083BD4E1E7009ED00A9B29C /* camera_play.png in Resources */, + F0E05A3B1EA0F9EB004B83FB /* tab_home@3x.png in Resources */, F083BDB61E7009ED00A9B29C /* remove_icon.png in Resources */, + F0614A131EDEE65000F5DC9A /* cancel.png in Resources */, F083BD281E7009ED00A9B29C /* animatedLogo-0.png in Resources */, F083BDB31E7009ED00A9B29C /* priorityLow.png in Resources */, F083BDA81E7009ED00A9B29C /* notificationsOff@2x.png in Resources */, F083BD7A1E7009ED00A9B29C /* edit_icon.png in Resources */, + F0E05A391EA0F9EB004B83FB /* tab_home.png in Resources */, F083BD361E7009ED00A9B29C /* call_audio_mute_on_icon.png in Resources */, F083BDCA1E7009ED00A9B29C /* search_icon@3x.png in Resources */, F083BDB41E7009ED00A9B29C /* priorityLow@2x.png in Resources */, F083BD671E7009ED00A9B29C /* directChatOff@3x.png in Resources */, F083BE791E7009ED00A9B29C /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, + F0E05A321EA0F9EB004B83FB /* tab_favourites_selected@3x.png in Resources */, F083BD4D1E7009ED00A9B29C /* camera_capture@3x.png in Resources */, F083BDB11E7009ED00A9B29C /* priorityHigh@2x.png in Resources */, + F0614A0E1EDDCCE700F5DC9A /* jump_to_unread@2x.png in Resources */, F083BDC21E7009ED00A9B29C /* scrollup.png in Resources */, F083BD521E7009ED00A9B29C /* camera_stop@2x.png in Resources */, + F0E05A381EA0F9EB004B83FB /* tab_home_selected@3x.png in Resources */, F083BDDA1E7009ED00A9B29C /* typing.png in Resources */, F083BE831E7009ED00A9B29C /* RecentTableViewCell.xib in Resources */, F083BDB71E7009ED00A9B29C /* remove_icon@2x.png in Resources */, @@ -1986,6 +2205,7 @@ F083BD3B1E7009ED00A9B29C /* call_chat_icon@3x.png in Resources */, F083BD351E7009ED00A9B29C /* call_audio_mute_off_icon@3x.png in Resources */, F083BE061E7009ED00A9B29C /* Riot-Defaults.plist in Resources */, + F0E05A441EA0F9EB004B83FB /* tab_rooms_selected@3x.png in Resources */, F083BD2C1E7009ED00A9B29C /* animatedLogo-4.png in Resources */, F083BD7E1E7009ED00A9B29C /* error@2x.png in Resources */, F083BE931E7009ED00A9B29C /* SimpleRoomTitleView.xib in Resources */, @@ -1996,21 +2216,31 @@ F083BD7D1E7009ED00A9B29C /* error.png in Resources */, F083BD2E1E7009ED00A9B29C /* back_icon@2x.png in Resources */, F083BD501E7009ED00A9B29C /* camera_record.png in Resources */, + F0D869EB1EC455A100BB0A2B /* create_direct_chat.png in Resources */, F083BDBB1E7009ED00A9B29C /* remove_icon_pink@3x.png in Resources */, + F0E05A301EA0F9EB004B83FB /* tab_favourites_selected.png in Resources */, F083BD5C1E7009ED00A9B29C /* create_room.png in Resources */, F083BE871E7009ED00A9B29C /* RoomTableViewCell.xib in Resources */, F083BD471E7009ED00A9B29C /* call_video_mute_off_icon@3x.png in Resources */, + F075BED71EBB169C00A7B68A /* RoomCollectionViewCell.xib in Resources */, F083BD941E7009ED00A9B29C /* group@3x.png in Resources */, F083BD591E7009ED00A9B29C /* chevron.png in Resources */, + F0D869EC1EC455A100BB0A2B /* create_direct_chat@2x.png in Resources */, F083BDE11E7009ED00A9B29C /* video_icon@2x.png in Resources */, F083BDB51E7009ED00A9B29C /* priorityLow@3x.png in Resources */, F083BE891E7009ED00A9B29C /* RoomMemberTitleView.xib in Resources */, F083BE3D1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, + F0E05A431EA0F9EB004B83FB /* tab_rooms_selected@2x.png in Resources */, F083BE291E7009ED00A9B29C /* StartChatViewController.xib in Resources */, F083BE6F1E7009ED00A9B29C /* RoomOutgoingAttachmentWithPaginationTitleBubbleCell.xib in Resources */, F083BEA31E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.xib in Resources */, + 32FD0A3E1EB0CD9B0072B066 /* BugReportViewController.xib in Resources */, + 32D392191EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib in Resources */, + F0614A151EDEE65000F5DC9A /* cancel@3x.png in Resources */, + F0E05A3E1EA0F9EB004B83FB /* tab_people_selected@3x.png in Resources */, F083BE5D1E7009ED00A9B29C /* RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, F083BD2B1E7009ED00A9B29C /* animatedLogo-3.png in Resources */, + F0E05A331EA0F9EB004B83FB /* tab_favourites.png in Resources */, F083BD791E7009ED00A9B29C /* e2e_warning@3x.png in Resources */, F083BD481E7009ED00A9B29C /* call_video_mute_on_icon.png in Resources */, F083BDD41E7009ED00A9B29C /* shrink_icon.png in Resources */, @@ -2021,6 +2251,7 @@ F083BE8F1E7009ED00A9B29C /* RoomAvatarTitleView.xib in Resources */, F083BD9E1E7009ED00A9B29C /* mod_icon.png in Resources */, F083BD371E7009ED00A9B29C /* call_audio_mute_on_icon@2x.png in Resources */, + F0E05A341EA0F9EB004B83FB /* tab_favourites@2x.png in Resources */, F083BD571E7009ED00A9B29C /* camera_video_capture@2x.png in Resources */, F083BD581E7009ED00A9B29C /* camera_video_capture@3x.png in Resources */, F083BE321E7009ED00A9B29C /* DeviceTableViewCell.xib in Resources */, @@ -2039,6 +2270,7 @@ F083BD251E7009ED00A9B29C /* admin_icon.png in Resources */, F083BDBE1E7009ED00A9B29C /* riot_icon@3x.png in Resources */, F083BD931E7009ED00A9B29C /* group@2x.png in Resources */, + F0614A0D1EDDCCE700F5DC9A /* jump_to_unread.png in Resources */, F083BDC11E7009ED00A9B29C /* scrolldown@3x.png in Resources */, F083BD321E7009ED00A9B29C /* bubbles_bg_landscape@3x.png in Resources */, F083BE7F1E7009ED00A9B29C /* InviteRecentTableViewCell.xib in Resources */, @@ -2056,6 +2288,7 @@ F083BE571E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.xib in Resources */, F083BE771E7009ED00A9B29C /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.xib in Resources */, F083BDA71E7009ED00A9B29C /* notificationsOff.png in Resources */, + F06CDD6A1EF01E3900870B75 /* RoomEmptyBubbleCell.xib in Resources */, F083BDBD1E7009ED00A9B29C /* riot_icon@2x.png in Resources */, F083BDDE1E7009ED00A9B29C /* upload_icon@2x.png in Resources */, F083BE8B1E7009ED00A9B29C /* ExpandedRoomTitleView.xib in Resources */, @@ -2067,14 +2300,17 @@ F083BDE31E7009ED00A9B29C /* voice_call_icon.png in Resources */, F083BDCC1E7009ED00A9B29C /* selection_tick@2x.png in Resources */, F083BD961E7009ED00A9B29C /* leave@2x.png in Resources */, + F0E05A461EA0F9EB004B83FB /* tab_rooms@2x.png in Resources */, F083BD851E7009ED00A9B29C /* favouriteOff@3x.png in Resources */, F083BD541E7009ED00A9B29C /* camera_switch@2x.png in Resources */, F083BD6A1E7009ED00A9B29C /* directChatOn@3x.png in Resources */, F083BE3B1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentBubbleCell.xib in Resources */, F083BD341E7009ED00A9B29C /* call_audio_mute_off_icon@2x.png in Resources */, F083BD6E1E7009ED00A9B29C /* e2e_blocked.png in Resources */, + F0E05A3A1EA0F9EB004B83FB /* tab_home@2x.png in Resources */, F083BE091E7009ED00A9B29C /* AuthenticationViewController.xib in Resources */, F083BD381E7009ED00A9B29C /* call_audio_mute_on_icon@3x.png in Resources */, + F0E05A3C1EA0F9EB004B83FB /* tab_people_selected.png in Resources */, F083BD221E7009ED00A9B29C /* add_participant.png in Resources */, F083BDC61E7009ED00A9B29C /* search_bg@2x.png in Resources */, F083BD421E7009ED00A9B29C /* call_speaker_on_icon.png in Resources */, @@ -2086,6 +2322,7 @@ F083BE491E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, 325E1C151E8D03950018D91E /* LaunchScreen.storyboard in Resources */, F083BD721E7009ED00A9B29C /* e2e_unencrypted@2x.png in Resources */, + F0D869ED1EC455A100BB0A2B /* create_direct_chat@3x.png in Resources */, F083BD531E7009ED00A9B29C /* camera_switch.png in Resources */, F083BD7C1E7009ED00A9B29C /* edit_icon@3x.png in Resources */, F083BDE01E7009ED00A9B29C /* video_icon.png in Resources */, @@ -2098,6 +2335,7 @@ F083BE851E7009ED00A9B29C /* RoomIdOrAliasTableViewCell.xib in Resources */, F083BD461E7009ED00A9B29C /* call_video_mute_off_icon@2x.png in Resources */, F083BD311E7009ED00A9B29C /* bubbles_bg_landscape@2x.png in Resources */, + F0E05A371EA0F9EB004B83FB /* tab_home_selected@2x.png in Resources */, F083BE651E7009ED00A9B29C /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, F083BD6F1E7009ED00A9B29C /* e2e_blocked@2x.png in Resources */, ); @@ -2155,7 +2393,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -2172,9 +2410,11 @@ F083BE021E7009ED00A9B29C /* AvatarGenerator.m in Sources */, F083BEA01E7009ED00A9B29C /* TableViewCellWithLabelAndLargeTextView.m in Sources */, F083BE1A1E7009ED00A9B29C /* RoomFilesSearchViewController.m in Sources */, + 32D392181EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.m in Sources */, F083BE9C1E7009ED00A9B29C /* TableViewCellWithCheckBoxAndLabel.m in Sources */, F083BDFE1E7009ED00A9B29C /* RecentCellData.m in Sources */, F083BE3A1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentBubbleCell.m in Sources */, + 3205ED841E97725E003D65FA /* DirectoryServerTableViewCell.m in Sources */, F083BEA21E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.m in Sources */, F083BE0A1E7009ED00A9B29C /* CallViewController.m in Sources */, F083BE111E7009ED00A9B29C /* DirectoryViewController.m in Sources */, @@ -2198,6 +2438,7 @@ F083BE741E7009ED00A9B29C /* RoomOutgoingTextMsgWithoutSenderNameBubbleCell.m in Sources */, F083BE1C1E7009ED00A9B29C /* RoomMemberDetailsViewController.m in Sources */, F083BE481E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, + F06CDD691EF01E3900870B75 /* RoomEmptyBubbleCell.m in Sources */, F083BE041E7009ED00A9B29C /* Tools.m in Sources */, F083BE6A1E7009ED00A9B29C /* RoomOutgoingAttachmentBubbleCell.m in Sources */, F083BDEF1E7009ED00A9B29C /* UINavigationController+Riot.m in Sources */, @@ -2207,7 +2448,9 @@ F083BE621E7009ED00A9B29C /* RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, F083BE881E7009ED00A9B29C /* RoomMemberTitleView.m in Sources */, F083BE701E7009ED00A9B29C /* RoomOutgoingTextMsgBubbleCell.m in Sources */, + 3205ED7D1E976C8A003D65FA /* DirectoryServerPickerViewController.m in Sources */, F083BE7C1E7009ED00A9B29C /* DirectoryRecentTableViewCell.m in Sources */, + F0E05A031E963103004B83FB /* RoomsViewController.m in Sources */, F083BE801E7009ED00A9B29C /* PublicRoomTableViewCell.m in Sources */, F083BE031E7009ED00A9B29C /* EventFormatter.m in Sources */, F083BE4A1E7009ED00A9B29C /* RoomOutgoingEncryptedAttachmentBubbleCell.m in Sources */, @@ -2223,18 +2466,23 @@ F083BE3E1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.m in Sources */, F083BE071E7009ED00A9B29C /* AttachmentsViewController.m in Sources */, F083BE051E7009ED00A9B29C /* RiotDesignValues.m in Sources */, + 32FD0A3D1EB0CD9B0072B066 /* BugReportViewController.m in Sources */, F083BE311E7009ED00A9B29C /* DeviceTableViewCell.m in Sources */, F083BE501E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgBubbleCell.m in Sources */, F083BDED1E7009ED00A9B29C /* MXKRoomBubbleTableViewCell+Riot.m in Sources */, + F05BD79E1E7AEBF800C69941 /* UnifiedSearchViewController.m in Sources */, F083BE561E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.m in Sources */, + F02C1A861E8EB04C0045A404 /* PeopleViewController.m in Sources */, F083BDFA1E7009ED00A9B29C /* RoomPreviewData.m in Sources */, F083BE151E7009ED00A9B29C /* MediaAlbumContentViewController.m in Sources */, F083BE5C1E7009ED00A9B29C /* RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m in Sources */, + F0E05A061E9682E9004B83FB /* ContactsDataSource.m in Sources */, F083BE401E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgBubbleCell.m in Sources */, F083BDFD1E7009ED00A9B29C /* PublicRoomsDirectoryDataSource.m in Sources */, F083BE191E7009ED00A9B29C /* RecentsViewController.m in Sources */, F083BE351E7009ED00A9B29C /* MediaAlbumTableCell.m in Sources */, F083BE4C1E7009ED00A9B29C /* RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.m in Sources */, + F075BEDB1EBB26F100A7B68A /* TableViewCellWithCollectionView.m in Sources */, F083BE001E7009ED00A9B29C /* FilesSearchCellData.m in Sources */, F083BE7A1E7009ED00A9B29C /* RoomInputToolbarView.m in Sources */, F083BDFF1E7009ED00A9B29C /* RecentsDataSource.m in Sources */, @@ -2259,6 +2507,8 @@ F083BE271E7009ED00A9B29C /* SettingsViewController.m in Sources */, F083BE9A1E7009ED00A9B29C /* TableViewCellWithButton.m in Sources */, F083BE8E1E7009ED00A9B29C /* RoomAvatarTitleView.m in Sources */, + F05BD7A11E7C0E4500C69941 /* MasterTabBarController.m in Sources */, + F0E059FD1E9545BB004B83FB /* UnifiedSearchRecentsDataSource.m in Sources */, F083BE4E1E7009ED00A9B29C /* RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.m in Sources */, F083BE6C1E7009ED00A9B29C /* RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m in Sources */, F083BE681E7009ED00A9B29C /* RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, @@ -2272,6 +2522,8 @@ F083BDF91E7009ED00A9B29C /* RoomEmailInvitation.m in Sources */, F083BE341E7009ED00A9B29C /* EncryptionInfoView.m in Sources */, F083BE641E7009ED00A9B29C /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.m in Sources */, + F075BED61EBB169C00A7B68A /* RoomCollectionViewCell.m in Sources */, + F0E05A021E963103004B83FB /* FavouritesViewController.m in Sources */, F083BE941E7009ED00A9B29C /* FilesSearchTableViewCell.m in Sources */, F083BE921E7009ED00A9B29C /* SimpleRoomTitleView.m in Sources */, F083BE981E7009ED00A9B29C /* MessagesSearchResultTextMsgBubbleCell.m in Sources */, diff --git a/Riot/API/RageShakeManager.h b/Riot/API/RageShakeManager.h index 10741397c..a220800b7 100644 --- a/Riot/API/RageShakeManager.h +++ b/Riot/API/RageShakeManager.h @@ -19,7 +19,7 @@ #import -@interface RageShakeManager : NSObject +@interface RageShakeManager : NSObject + (id)sharedManager; diff --git a/Riot/API/RageShakeManager.m b/Riot/API/RageShakeManager.m index 7dec6fd03..ea94c5909 100644 --- a/Riot/API/RageShakeManager.m +++ b/Riot/API/RageShakeManager.m @@ -19,8 +19,7 @@ #import "RageShakeManager.h" #import "AppDelegate.h" - -#import "GBDeviceInfo_iOS.h" +#import "BugReportViewController.h" #import "NSBundle+MatrixKit.h" @@ -31,8 +30,6 @@ static RageShakeManager* sharedInstance = nil; double startShakingTimeStamp; MXKAlert *confirmationAlert; - - MFMailComposeViewController* mailComposer; } @end @@ -56,9 +53,9 @@ static RageShakeManager* sharedInstance = nil; if (self) { isShaking = NO; startShakingTimeStamp = 0; - - mailComposer = nil; + confirmationAlert = nil; + } return self; @@ -81,8 +78,10 @@ static RageShakeManager* sharedInstance = nil; [confirmationAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { typeof(self) self = weakSelf; self->confirmationAlert = nil; - - [self sendEmail:viewController withSnapshot:NO]; + + BugReportViewController *bugReportViewController = [BugReportViewController bugReportViewController]; + bugReportViewController.reportCrash = YES; + [bugReportViewController showInViewController:viewController]; }]; [confirmationAlert showInViewController:viewController]; @@ -121,7 +120,14 @@ static RageShakeManager* sharedInstance = nil; [confirmationAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { typeof(self) self = weakSelf; self->confirmationAlert = nil; - [self sendEmail:(UIViewController*)responder withSnapshot:YES]; + + UIViewController *controller = (UIViewController*)responder; + if (controller) { + + BugReportViewController *bugReportViewController = [BugReportViewController bugReportViewController]; + bugReportViewController.screenshot = [self takeScreenshot]; + [bugReportViewController showInViewController:controller]; + } }]; [confirmationAlert showInViewController:(UIViewController*)responder]; @@ -137,143 +143,48 @@ static RageShakeManager* sharedInstance = nil; } /** - Prepare and send a report email. The mail composer is presented by the provided view controller. - - @param controller the view controller which presents the alert. - @param snapshot if this boolean value is YES, a screenshot of `controller` is sent as email attachment + Take a screenshot of the current screen. + + @return an image */ -- (void)sendEmail:(UIViewController*)controller withSnapshot:(BOOL)snapshot { +- (UIImage*)takeScreenshot { UIImage *image; - if (snapshot) { - AppDelegate* theDelegate = [AppDelegate theDelegate]; - UIGraphicsBeginImageContextWithOptions(theDelegate.window.bounds.size, NO, [UIScreen mainScreen].scale); + AppDelegate* theDelegate = [AppDelegate theDelegate]; + UIGraphicsBeginImageContextWithOptions(theDelegate.window.bounds.size, NO, [UIScreen mainScreen].scale); - // Iterate over every window from back to front - for (UIWindow *window in [[UIApplication sharedApplication] windows]) + // Iterate over every window from back to front + for (UIWindow *window in [[UIApplication sharedApplication] windows]) + { + if (![window respondsToSelector:@selector(screen)] || [window screen] == [UIScreen mainScreen]) { - if (![window respondsToSelector:@selector(screen)] || [window screen] == [UIScreen mainScreen]) - { - // -renderInContext: renders in the coordinate space of the layer, - // so we must first apply the layer's geometry to the graphics context - CGContextSaveGState(UIGraphicsGetCurrentContext()); - // Center the context around the window's anchor point - CGContextTranslateCTM(UIGraphicsGetCurrentContext(), [window center].x, [window center].y); - // Apply the window's transform about the anchor point - CGContextConcatCTM(UIGraphicsGetCurrentContext(), [window transform]); - // Offset by the portion of the bounds left of and above the anchor point - CGContextTranslateCTM(UIGraphicsGetCurrentContext(), - -[window bounds].size.width * [[window layer] anchorPoint].x, - -[window bounds].size.height * [[window layer] anchorPoint].y); + // -renderInContext: renders in the coordinate space of the layer, + // so we must first apply the layer's geometry to the graphics context + CGContextSaveGState(UIGraphicsGetCurrentContext()); + // Center the context around the window's anchor point + CGContextTranslateCTM(UIGraphicsGetCurrentContext(), [window center].x, [window center].y); + // Apply the window's transform about the anchor point + CGContextConcatCTM(UIGraphicsGetCurrentContext(), [window transform]); + // Offset by the portion of the bounds left of and above the anchor point + CGContextTranslateCTM(UIGraphicsGetCurrentContext(), + -[window bounds].size.width * [[window layer] anchorPoint].x, + -[window bounds].size.height * [[window layer] anchorPoint].y); - // Render the layer hierarchy to the current context - [[window layer] renderInContext:UIGraphicsGetCurrentContext()]; + // Render the layer hierarchy to the current context + [[window layer] renderInContext:UIGraphicsGetCurrentContext()]; - // Restore the context - CGContextRestoreGState(UIGraphicsGetCurrentContext()); - } + // Restore the context + CGContextRestoreGState(UIGraphicsGetCurrentContext()); } - image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - // the image is copied in the clipboard - [UIPasteboard generalPasteboard].image = image; } - - if (controller) { + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); - NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]; + // the image is copied in the clipboard + [UIPasteboard generalPasteboard].image = image; - mailComposer = [[MFMailComposeViewController alloc] init]; - - NSString* appVersion = [AppDelegate theDelegate].appVersion; - NSString* build = [AppDelegate theDelegate].build; - - NSMutableString* subject; - if ([MXLogger crashLog]) { - subject = [NSMutableString stringWithFormat:@"[iOS] %@ crash report - %@", appDisplayName, appVersion]; - } - else { - subject = [NSMutableString stringWithFormat:@"[iOS] %@ bug report - %@", appDisplayName, appVersion]; - } - - // Add the build version to the subject if the app does not come from app store - if (![build containsString:@"master"]) { - [subject appendFormat:@" (%@)", build]; - } - - [mailComposer setSubject:subject]; - - [mailComposer setToRecipients:[NSArray arrayWithObject:@"rageshake@riot.im"]]; - - NSMutableString* message = [[NSMutableString alloc] init]; - - [message appendFormat:@"Something went wrong on my Matrix client: \n\n\n"]; - - [message appendFormat:@"-----> my comments <-----\n\n\n"]; - - [message appendFormat:@"------------------------------\n"]; - [message appendFormat:@"Account info\n"]; - - NSArray *mxAccounts = [MXKAccountManager sharedManager].accounts; - for (MXKAccount* account in mxAccounts) { - NSString *disabled = account.disabled ? @" (disabled)" : @""; - - [message appendFormat:@"user id: %@%@\n", account.mxCredentials.userId, disabled]; - if (account.mxSession.myUser.displayname) - { - [message appendFormat:@"displayname: %@\n", account.mxSession.myUser.displayname]; - } - - [message appendFormat:@"homeServerURL: %@\n", account.mxCredentials.homeServer]; - - // e2e information - [message appendFormat:@"e2e device id : %@\n", account.mxCredentials.deviceId]; - [message appendFormat:@"e2e device key: %@\n", account.mxSession.crypto.deviceEd25519Key]; - } - - [message appendFormat:@"------------------------------\n"]; - [message appendFormat:@"Application info\n"]; - [message appendString:appDisplayName];[message appendFormat:@" version: %@\n", appVersion]; - [message appendFormat:@"MatrixKit version: %@\n", MatrixKitVersion]; - [message appendFormat:@"MatrixSDK version: %@\n", MatrixSDKVersion]; - if (build.length) { - [message appendFormat:@"Build: %@\n", build]; - } - [message appendFormat:@"------------------------------\n"]; - [message appendFormat:@"Device info\n"]; - [message appendFormat:@"model: %@\n", [GBDeviceInfo deviceInfo].modelString]; - [message appendFormat:@"operatingSystem: %@ %@\n", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion]]; - - [mailComposer setMessageBody:message isHTML:NO]; - - // Attach image only if required - if (image) { - [mailComposer addAttachmentData:UIImageJPEGRepresentation(image, 1.0) mimeType:@"image/jpg" fileName:@"screenshot.jpg"]; - } - - // Add logs files - NSMutableArray *logFiles = [NSMutableArray arrayWithArray:[MXLogger logFiles]]; - if ([MXLogger crashLog]) { - [logFiles addObject:[MXLogger crashLog]]; - } - for (NSString *logFile in logFiles) { - NSData *logContent = [NSData dataWithContentsOfFile:logFile]; - [mailComposer addAttachmentData:logContent mimeType:@"text/plain" fileName:[logFile lastPathComponent]]; - } - mailComposer.mailComposeDelegate = self; - [controller presentViewController:mailComposer animated:YES completion:nil]; - } -} - -#pragma mark - MFMailComposeViewControllerDelegate delegate - -- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { - // Do not send this crash anymore - [MXLogger deleteCrashLog]; - - [controller dismissViewControllerAnimated:NO completion:nil]; + return image; } @end diff --git a/Riot/AppDelegate.h b/Riot/AppDelegate.h index bd9deb924..58929e7f3 100644 --- a/Riot/AppDelegate.h +++ b/Riot/AppDelegate.h @@ -1,5 +1,6 @@ /* Copyright 2014 OpenMarket Ltd + Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,7 +23,11 @@ #import "GAIFields.h" #import "GAIDictionaryBuilder.h" -#import "HomeViewController.h" +#import "MasterTabBarController.h" + +#import "RageShakeManager.h" + +#import "RiotDesignValues.h" #pragma mark - Notifications /** @@ -46,7 +51,7 @@ extern NSString *const kAppDelegateNetworkStatusDidChangeNotification; /** Application main view controller */ -@property (nonatomic, readonly) HomeViewController *homeViewController; +@property (nonatomic, readonly) MasterTabBarController *masterTabBarController; @property (strong, nonatomic) UIWindow *window; @@ -61,7 +66,7 @@ extern NSString *const kAppDelegateNetworkStatusDidChangeNotification; /** The navigation controller of the master view controller of the main split view controller. */ -@property (nonatomic, readonly) UINavigationController *homeNavigationController; +@property (nonatomic, readonly) UINavigationController *masterNavigationController; /** The navigation controller of the detail view controller of the main split view controller (may be nil). */ diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 54a01c9e5..72c8def0f 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -22,15 +22,12 @@ #import "EventFormatter.h" -#import "HomeViewController.h" #import "RoomViewController.h" #import "DirectoryViewController.h" #import "SettingsViewController.h" #import "ContactDetailsViewController.h" -#import "RageShakeManager.h" - #import "NSBundle+MatrixKit.h" #import "MatrixSDK/MatrixSDK.h" @@ -53,7 +50,7 @@ #include -#import "RiotDesignValues.h" +#include #define CALL_STATUS_BAR_HEIGHT 44 @@ -302,54 +299,24 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN mxSessionArray = [NSMutableArray array]; callEventsListeners = [NSMutableDictionary dictionary]; - // To simplify navigation into the app, we retrieve here the navigation controller and the view controller related - // to the Home screen ("Messages"). - // Note: UISplitViewController is not supported on iPhone for iOS < 8.0 - UIViewController* rootViewController = self.window.rootViewController; - _homeNavigationController = nil; - if ([rootViewController isKindOfClass:[UISplitViewController class]]) - { - UISplitViewController *splitViewController = (UISplitViewController *)rootViewController; - splitViewController.delegate = self; - - _homeNavigationController = [splitViewController.viewControllers objectAtIndex:0]; - - if (splitViewController.viewControllers.count == 2) - { - UIViewController *detailsViewController = [splitViewController.viewControllers lastObject]; - - if ([detailsViewController isKindOfClass:[UINavigationController class]]) - { - UINavigationController *navigationController = (UINavigationController*)detailsViewController; - detailsViewController = navigationController.topViewController; - } - - detailsViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem; - } - - // on IOS 8 iPad devices, force to display the primary and the secondary viewcontroller - // to avoid empty room View Controller in portrait orientation - // else, the user cannot select a room - // shouldHideViewController delegate method is also implemented - if ([splitViewController respondsToSelector:@selector(preferredDisplayMode)] && [(NSString*)[UIDevice currentDevice].model hasPrefix:@"iPad"]) - { - splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; - } - } + // To simplify navigation into the app, we retrieve here the main navigation controller and the tab bar controller. + UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; + splitViewController.delegate = self; - if (_homeNavigationController) + _masterNavigationController = [splitViewController.viewControllers objectAtIndex:0]; + _masterTabBarController = _masterNavigationController.viewControllers.firstObject; + + // on IOS 8 iPad devices, force to display the primary and the secondary viewcontroller + // to avoid empty room View Controller in portrait orientation + // else, the user cannot select a room + // shouldHideViewController delegate method is also implemented + if ([(NSString*)[UIDevice currentDevice].model hasPrefix:@"iPad"]) { - for (UIViewController *viewController in _homeNavigationController.viewControllers) - { - if ([viewController isKindOfClass:[HomeViewController class]]) - { - _homeViewController = (HomeViewController*)viewController; - } - } + splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; } // Sanity check - NSAssert(_homeViewController, @"Something wrong in Main.storyboard"); + NSAssert(_masterTabBarController, @"Something wrong in Main.storyboard"); _isAppForeground = NO; @@ -563,12 +530,6 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Suspend error notifications during navigation stack change. isErrorNotificationSuspended = YES; - // Cancel search - if (_homeViewController) - { - [_homeViewController hideSearch:NO]; - } - // Dismiss potential view controllers that were presented modally (like the media picker). if (self.window.rootViewController.presentedViewController) { @@ -720,16 +681,16 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN if (weakSelf) { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->cryptoDataCorruptedAlert = nil; + typeof(self) self = weakSelf; + self->cryptoDataCorruptedAlert = nil; } }]; [cryptoDataCorruptedAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"settings_sign_out"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->cryptoDataCorruptedAlert = nil; + typeof(self) self = weakSelf; + self->cryptoDataCorruptedAlert = nil; [[MXKAccountManager sharedManager] removeAccount:account completion:nil]; @@ -749,18 +710,21 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [secondNavController popToRootViewControllerAnimated:animated]; } - // Force back to the main screen if this is the not the one that is displayed - if (_homeViewController && _homeViewController != _homeNavigationController.visibleViewController) + // Force back to the main screen if this is not the one that is displayed + if (_masterTabBarController && _masterTabBarController != _masterNavigationController.visibleViewController) { - // Listen to the homeNavigationController changes - // We need to be sure that homeViewController is back to the screen + // Listen to the masterNavigationController changes + // We need to be sure that masterTabBarController is back to the screen popToHomeViewControllerCompletion = completion; - _homeNavigationController.delegate = self; + _masterNavigationController.delegate = self; - [_homeNavigationController popToViewController:_homeViewController animated:animated]; + [_masterNavigationController popToViewController:_masterTabBarController animated:animated]; } else { + // Select the Home tab + _masterTabBarController.selectedIndex = TABBAR_HOME_INDEX; + if (completion) { completion(); @@ -772,18 +736,18 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { - if (viewController == _homeViewController) + if (viewController == _masterTabBarController) { - _homeNavigationController.delegate = nil; + _masterNavigationController.delegate = nil; // For unknown reason, the navigation bar is not restored correctly by [popToViewController:animated:] // when a ViewController has hidden it (see MXKAttachmentsViewController). // Patch: restore navigation bar by default here. - _homeNavigationController.navigationBarHidden = NO; - - // Release the current selected room (if any). - [_homeViewController closeSelectedRoom]; + _masterNavigationController.navigationBarHidden = NO; + // Release the current selected item (room/contact/...). + [_masterTabBarController releaseSelectedItem]; + if (popToHomeViewControllerCompletion) { void (^popToHomeViewControllerCompletion2)() = popToHomeViewControllerCompletion; @@ -1033,11 +997,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)refreshApplicationIconBadgeNumber { - NSUInteger count = 0; - for (MXSession *session in mxSessionArray) - { - count += [session missedDiscussionsCount]; - } + // Consider the total number of missed discussions including the invites. + NSUInteger count = [self.masterTabBarController missedDiscussionsCount]; NSLog(@"[AppDelegate] refreshApplicationIconBadgeNumber: %tu", count); @@ -1178,101 +1139,107 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // We will display something but we need to do some requests before. // So, come back to the home VC and show its loading wheel while processing [self restoreInitialDisplay:^{ - - [_homeViewController startActivityIndicator]; - - if ([roomIdOrAlias hasPrefix:@"#"]) + + if ([_masterTabBarController.selectedViewController isKindOfClass:MXKViewController.class]) { - // The alias may be not part of user's rooms states - // Ask the HS to resolve the room alias into a room id and then retry - universalLinkFragmentPending = fragment; - MXKAccount* account = accountManager.activeAccounts.firstObject; - [account.mxSession.matrixRestClient roomIDForRoomAlias:roomIdOrAlias success:^(NSString *roomId) { - - // Note: the activity indicator will not disappear if the session is not ready - [_homeViewController stopActivityIndicator]; - - // Check that 'fragment' has not been cancelled - if ([universalLinkFragmentPending isEqualToString:fragment]) - { - // Retry opening the link but with the returned room id - NSString *newUniversalLinkFragment = - [fragment stringByReplacingOccurrencesOfString:[roomIdOrAlias stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] - withString:[roomId stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - - universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; - - [self handleUniversalLinkFragment:newUniversalLinkFragment]; - } - - } failure:^(NSError *error) { - NSLog(@"[AppDelegate] Universal link: Error: The home server failed to resolve the room alias (%@)", roomIdOrAlias); - }]; - } - else if ([roomIdOrAlias hasPrefix:@"!"] && ((MXKAccount*)accountManager.activeAccounts.firstObject).mxSession.state != MXSessionStateRunning) - { - // The user does not know the room id but this may be because their session is not yet sync'ed - // So, wait for the completion of the sync and then retry - // FIXME: Manange all user's accounts not only the first one - MXKAccount* account = accountManager.activeAccounts.firstObject; - - NSLog(@"[AppDelegate] Universal link: Need to wait for the session to be sync'ed and running"); - universalLinkFragmentPending = fragment; - - universalLinkWaitingObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull notif) { - - // Check that 'fragment' has not been cancelled - if ([universalLinkFragmentPending isEqualToString:fragment]) - { - // Check whether the concerned session is the associated one - if (notif.object == account.mxSession && account.mxSession.state == MXSessionStateRunning) - { - NSLog(@"[AppDelegate] Universal link: The session is running. Retry the link"); - [self handleUniversalLinkFragment:fragment]; - } - } - }]; - } - else - { - NSLog(@"[AppDelegate] Universal link: The room (%@) is not known by any account (email invitation: %@). Display its preview to try to join it", roomIdOrAlias, queryParams ? @"YES" : @"NO"); - - // FIXME: In case of multi-account, ask the user which one to use - MXKAccount* account = accountManager.activeAccounts.firstObject; - - RoomPreviewData *roomPreviewData; - if (queryParams) + MXKViewController *homeViewController = (MXKViewController*)_masterTabBarController.selectedViewController; + + [homeViewController startActivityIndicator]; + + if ([roomIdOrAlias hasPrefix:@"#"]) { - // Note: the activity indicator will not disappear if the session is not ready - [_homeViewController stopActivityIndicator]; - - roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias emailInvitationParams:queryParams andSession:account.mxSession]; - [self showRoomPreview:roomPreviewData]; + // The alias may be not part of user's rooms states + // Ask the HS to resolve the room alias into a room id and then retry + universalLinkFragmentPending = fragment; + MXKAccount* account = accountManager.activeAccounts.firstObject; + [account.mxSession.matrixRestClient roomIDForRoomAlias:roomIdOrAlias success:^(NSString *roomId) { + + // Note: the activity indicator will not disappear if the session is not ready + [homeViewController stopActivityIndicator]; + + // Check that 'fragment' has not been cancelled + if ([universalLinkFragmentPending isEqualToString:fragment]) + { + // Retry opening the link but with the returned room id + NSString *newUniversalLinkFragment = + [fragment stringByReplacingOccurrencesOfString:[roomIdOrAlias stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] + withString:[roomId stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + + universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; + + [self handleUniversalLinkFragment:newUniversalLinkFragment]; + } + + } failure:^(NSError *error) { + NSLog(@"[AppDelegate] Universal link: Error: The home server failed to resolve the room alias (%@)", roomIdOrAlias); + }]; + } + else if ([roomIdOrAlias hasPrefix:@"!"] && ((MXKAccount*)accountManager.activeAccounts.firstObject).mxSession.state != MXSessionStateRunning) + { + // The user does not know the room id but this may be because their session is not yet sync'ed + // So, wait for the completion of the sync and then retry + // FIXME: Manange all user's accounts not only the first one + MXKAccount* account = accountManager.activeAccounts.firstObject; + + NSLog(@"[AppDelegate] Universal link: Need to wait for the session to be sync'ed and running"); + universalLinkFragmentPending = fragment; + + universalLinkWaitingObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull notif) { + + // Check that 'fragment' has not been cancelled + if ([universalLinkFragmentPending isEqualToString:fragment]) + { + // Check whether the concerned session is the associated one + if (notif.object == account.mxSession && account.mxSession.state == MXSessionStateRunning) + { + NSLog(@"[AppDelegate] Universal link: The session is running. Retry the link"); + [self handleUniversalLinkFragment:fragment]; + } + } + }]; } else { - roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias andSession:account.mxSession]; - - // Is it a link to an event of a room? - // If yes, the event will be displayed once the room is joined - roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil; - - // Try to get more information about the room before opening its preview - [roomPreviewData peekInRoom:^(BOOL succeeded) { - + NSLog(@"[AppDelegate] Universal link: The room (%@) is not known by any account (email invitation: %@). Display its preview to try to join it", roomIdOrAlias, queryParams ? @"YES" : @"NO"); + + // FIXME: In case of multi-account, ask the user which one to use + MXKAccount* account = accountManager.activeAccounts.firstObject; + + RoomPreviewData *roomPreviewData; + if (queryParams) + { // Note: the activity indicator will not disappear if the session is not ready - [_homeViewController stopActivityIndicator]; + [homeViewController stopActivityIndicator]; - // If no data is available for this room, we name it with the known room alias (if any). - if (!succeeded && universalLinkFragmentPendingRoomAlias[roomIdOrAlias]) - { - roomPreviewData.roomName = universalLinkFragmentPendingRoomAlias[roomIdOrAlias]; - } - universalLinkFragmentPendingRoomAlias = nil; - + roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias emailInvitationParams:queryParams andSession:account.mxSession]; [self showRoomPreview:roomPreviewData]; - }]; + } + else + { + roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias andSession:account.mxSession]; + + // Is it a link to an event of a room? + // If yes, the event will be displayed once the room is joined + roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil; + + // Try to get more information about the room before opening its preview + [roomPreviewData peekInRoom:^(BOOL succeeded) { + + // Note: the activity indicator will not disappear if the session is not ready + [homeViewController stopActivityIndicator]; + + // If no data is available for this room, we name it with the known room alias (if any). + if (!succeeded && universalLinkFragmentPendingRoomAlias[roomIdOrAlias]) + { + roomPreviewData.roomName = universalLinkFragmentPendingRoomAlias[roomIdOrAlias]; + } + universalLinkFragmentPendingRoomAlias = nil; + + [self showRoomPreview:roomPreviewData]; + }]; + } } + } }]; @@ -1304,7 +1271,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN NSLog(@"[AppDelegate] Universal link with registration parameters"); continueUserActivity = YES; - [_homeViewController showAuthenticationScreenWithRegistrationParameters:queryParams]; + [_masterTabBarController showAuthenticationScreenWithRegistrationParameters:queryParams]; } else { @@ -1392,18 +1359,23 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)initMatrixSessions { NSLog(@"[AppDelegate] initMatrixSessions"); + + MXSDKOptions *sdkOptions = [MXSDKOptions sharedInstance]; // Define the media cache version - [MXSDKOptions sharedInstance].mediaCacheAppVersion = 0; + sdkOptions.mediaCacheAppVersion = 0; // Enable e2e encryption for newly created MXSession - [MXSDKOptions sharedInstance].enableCryptoWhenStartingMXSession = YES; + sdkOptions.enableCryptoWhenStartingMXSession = YES; // Disable identicon use - [MXSDKOptions sharedInstance].disableIdenticonUseForUserAvatar = YES; + sdkOptions.disableIdenticonUseForUserAvatar = YES; // Enable SDK stats upload to GA - [MXSDKOptions sharedInstance].enableGoogleAnalytics = YES; + sdkOptions.enableGoogleAnalytics = YES; + + // Use UIKit BackgroundTask for handling background tasks in the SDK + sdkOptions.backgroundModeHandler = [[MXUIKitBackgroundModeHandler alloc] init]; // Disable long press on event in bubble cells [MXKRoomBubbleTableViewCell disableLongPressGestureOnEvent:YES]; @@ -1633,9 +1605,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [[MXKContactManager sharedManager] addMatrixSession:mxSession]; // Update home data sources - [_homeViewController addMatrixSession:mxSession]; + [_masterTabBarController addMatrixSession:mxSession]; [mxSessionArray addObject:mxSession]; + + // Do the one time check on device id + [self checkDeviceId:mxSession]; } } @@ -1644,7 +1619,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [[MXKContactManager sharedManager] removeMatrixSession:mxSession]; // Update home data sources - [_homeViewController removeMatrixSession:mxSession]; + [_masterTabBarController removeMatrixSession:mxSession]; // If any, disable the no VoIP support workaround [self disableNoVoIPOnMatrixSession:mxSession]; @@ -1707,7 +1682,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [[MXKAccountManager sharedManager] logout]; // Return to authentication screen - [_homeViewController showAuthenticationScreen]; + [_masterTabBarController showAuthenticationScreen]; // Note: Keep App settings @@ -1776,18 +1751,18 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN if (weakSelf) { - __strong __typeof(weakSelf)strongSelf = weakSelf; + typeof(self) self = weakSelf; // Reject the call. // Note: Do not reset the incoming call notification before this operation, because it is used to release properly the dismissed call view controller. - if (strongSelf->currentCallViewController) + if (self->currentCallViewController) { - [strongSelf->currentCallViewController onButtonPressed:strongSelf->currentCallViewController.rejectCallButton]; + [self->currentCallViewController onButtonPressed:self->currentCallViewController.rejectCallButton]; currentCallViewController = nil; } - strongSelf.incomingCallNotification = nil; + self.incomingCallNotification = nil; mxCall.delegate = nil; } @@ -1799,15 +1774,15 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN handler:^(MXKAlert *alert) { if (weakSelf) { - __strong __typeof(weakSelf)strongSelf = weakSelf; + typeof(self) self = weakSelf; - strongSelf.incomingCallNotification = nil; + self.incomingCallNotification = nil; - if (strongSelf->currentCallViewController) + if (self->currentCallViewController) { - [strongSelf->currentCallViewController onButtonPressed:strongSelf->currentCallViewController.answerCallButton]; + [self->currentCallViewController onButtonPressed:self->currentCallViewController.answerCallButton]; - [strongSelf presentCallViewController:nil]; + [self presentCallViewController:nil]; } } @@ -1841,7 +1816,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN break; case MXSessionStateStoreDataReady: case MXSessionStateSyncInProgress: - isLaunching = (mainSession.rooms.count == 0); + // Stay in launching during the first server sync if the store is empty. + isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); default: break; } @@ -1980,6 +1956,61 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } } +#pragma mark - + +/** + Check the existence of device id. + */ +- (void)checkDeviceId:(MXSession*)mxSession +{ + // In case of the app update for the e2e encryption, the app starts with + // no device id provided by the homeserver. + // Ask the user to login again in order to enable e2e. Ask it once + if (!isErrorNotificationSuspended && ![[NSUserDefaults standardUserDefaults] boolForKey:@"deviceIdAtStartupChecked"]) + { + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"deviceIdAtStartupChecked"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + // Check if there is a device id + if (!mxSession.matrixRestClient.credentials.deviceId) + { + NSLog(@"WARNING: The user has no device. Prompt for login again"); + + NSString *msg = NSLocalizedStringFromTable(@"e2e_enabling_on_app_update", @"Vector", nil); + + __weak typeof(self) weakSelf = self; + [_errorNotification dismiss:NO]; + _errorNotification = [[MXKAlert alloc] initWithTitle:nil message:msg style:MXKAlertStyleAlert]; + + _errorNotification.cancelButtonIndex = [_errorNotification addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"later"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->_errorNotification = nil; + } + + }]; + + [_errorNotification addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->_errorNotification = nil; + + [self logout]; + } + + }]; + + // Prompt the user + _errorNotification.mxkAccessibilityIdentifier = @"AppDelegateErrorAlert"; + [self showNotificationAlert:_errorNotification]; + } + } +} + #pragma mark - Matrix Accounts handling - (void)enableInAppNotificationsForAccount:(MXKAccount*)account @@ -2082,8 +2113,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { [accountPicker addActionWithTitle:account.mxCredentials.userId style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->accountPicker = nil; + typeof(self) self = weakSelf; + self->accountPicker = nil; if (onSelection) { @@ -2094,8 +2125,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN accountPicker.cancelButtonIndex = [accountPicker addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->accountPicker = nil; + typeof(self) self = weakSelf; + self->accountPicker = nil; if (onSelection) { @@ -2114,7 +2145,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [self restoreInitialDisplay:^{ // Select room to display its details (dispatch this action in order to let TabBarController end its refresh) - [_homeViewController selectRoomWithId:roomId andEventId:eventId inMatrixSession:mxSession]; + [_masterTabBarController selectRoomWithId:roomId andEventId:eventId inMatrixSession:mxSession]; }]; } @@ -2122,7 +2153,9 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (void)showRoomPreview:(RoomPreviewData*)roomPreviewData { [self restoreInitialDisplay:^{ - [_homeViewController showRoomPreview:roomPreviewData]; + + [_masterTabBarController showRoomPreview:roomPreviewData]; + }]; } @@ -2280,8 +2313,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN _incomingCallNotification = nil; // Release properly - currentCallViewController.mxCall.delegate = nil; - currentCallViewController.delegate = nil; + [currentCallViewController destroy]; currentCallViewController = nil; if (completion) @@ -2329,8 +2361,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN [self removeCallStatusBar]; // Release properly - currentCallViewController.mxCall.delegate = nil; - currentCallViewController.delegate = nil; + [currentCallViewController destroy]; currentCallViewController = nil; } } @@ -2524,45 +2555,26 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN - (nullable UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController { - UIViewController *topViewController = _homeNavigationController.topViewController; - - // Check the case where we don't want to use as a secondary view controller the top view controller - // of the navigation controller of the home view controller. - if ([topViewController isKindOfClass:[DirectoryViewController class]] - || [topViewController isKindOfClass:[SettingsViewController class]] - || [topViewController isKindOfClass:[ContactDetailsViewController class]]) + // Return the top view controller of the master navigation controller, if it is a navigation controller itself. + UIViewController *topViewController = _masterNavigationController.topViewController; + if ([topViewController isKindOfClass:UINavigationController.class]) { - UINavigationController *secondNavController = self.secondaryNavigationController; - if (secondNavController) - { - // Return the default secondary view controller to keep on primaryViewController side - // the Directory, the Settings or the Contact details view controller. - return secondNavController; - } - else - { - // Return a fake room view controller for the secondary view controller. - return [RoomViewController roomViewController]; - } + return topViewController; } - return nil; + + // Else return the default empty details view controller from the storyboard. + // Be sure that the primary is then visible too. + if (splitViewController.displayMode == UISplitViewControllerDisplayModePrimaryHidden) + { + splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; + } + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; + return [storyboard instantiateViewControllerWithIdentifier:@"EmptyDetailsViewControllerStoryboardId"]; } - (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController { - RoomViewController *roomViewController; - - if ([secondaryViewController isKindOfClass:[RoomViewController class]]) - { - roomViewController = (RoomViewController*)secondaryViewController; - } - else if ([secondaryViewController isKindOfClass:[UINavigationController class]] && - [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[RoomViewController class]]) - { - roomViewController = (RoomViewController*)[(UINavigationController *)secondaryViewController topViewController]; - } - - if (roomViewController && roomViewController.roomDataSource == nil && roomViewController.roomPreviewData == nil) + if (!self.masterTabBarController.currentRoomViewController && !self.masterTabBarController.currentContactDetailViewController) { // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. return YES; @@ -2654,8 +2666,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN noCallSupportAlert.cancelButtonIndex = [noCallSupportAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ignore"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->noCallSupportAlert = nil; + typeof(self) self = weakSelf; + self->noCallSupportAlert = nil; }]; @@ -2671,8 +2683,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN NSLog(@"[AppDelegate] enableNoVoIPOnMatrixSession: ERROR: Cannot send m.call.hangup event."); }]; - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->noCallSupportAlert = nil; + typeof(self) self = weakSelf; + self->noCallSupportAlert = nil; }]; diff --git a/Riot/Assets/Images/cancel.png b/Riot/Assets/Images/cancel.png new file mode 100644 index 000000000..f845e00cd Binary files /dev/null and b/Riot/Assets/Images/cancel.png differ diff --git a/Riot/Assets/Images/cancel@2x.png b/Riot/Assets/Images/cancel@2x.png new file mode 100644 index 000000000..f20238e8f Binary files /dev/null and b/Riot/Assets/Images/cancel@2x.png differ diff --git a/Riot/Assets/Images/cancel@3x.png b/Riot/Assets/Images/cancel@3x.png new file mode 100644 index 000000000..6d9a1af3c Binary files /dev/null and b/Riot/Assets/Images/cancel@3x.png differ diff --git a/Riot/Assets/Images/create_direct_chat.png b/Riot/Assets/Images/create_direct_chat.png new file mode 100644 index 000000000..133274cc2 Binary files /dev/null and b/Riot/Assets/Images/create_direct_chat.png differ diff --git a/Riot/Assets/Images/create_direct_chat@2x.png b/Riot/Assets/Images/create_direct_chat@2x.png new file mode 100644 index 000000000..4e07815dc Binary files /dev/null and b/Riot/Assets/Images/create_direct_chat@2x.png differ diff --git a/Riot/Assets/Images/create_direct_chat@3x.png b/Riot/Assets/Images/create_direct_chat@3x.png new file mode 100644 index 000000000..b98989ca0 Binary files /dev/null and b/Riot/Assets/Images/create_direct_chat@3x.png differ diff --git a/Riot/Assets/Images/directChatOff.png b/Riot/Assets/Images/directChatOff.png index 3b6e6d26e..f1fc0f649 100755 Binary files a/Riot/Assets/Images/directChatOff.png and b/Riot/Assets/Images/directChatOff.png differ diff --git a/Riot/Assets/Images/directChatOff@2x.png b/Riot/Assets/Images/directChatOff@2x.png index 5228f85fd..a3eac676c 100755 Binary files a/Riot/Assets/Images/directChatOff@2x.png and b/Riot/Assets/Images/directChatOff@2x.png differ diff --git a/Riot/Assets/Images/directChatOff@3x.png b/Riot/Assets/Images/directChatOff@3x.png index c571a89c8..176f89edc 100755 Binary files a/Riot/Assets/Images/directChatOff@3x.png and b/Riot/Assets/Images/directChatOff@3x.png differ diff --git a/Riot/Assets/Images/directChatOn.png b/Riot/Assets/Images/directChatOn.png index 8a19edac5..f751c6c06 100755 Binary files a/Riot/Assets/Images/directChatOn.png and b/Riot/Assets/Images/directChatOn.png differ diff --git a/Riot/Assets/Images/directChatOn@2x.png b/Riot/Assets/Images/directChatOn@2x.png index 07bd6dc1b..aa575b59a 100755 Binary files a/Riot/Assets/Images/directChatOn@2x.png and b/Riot/Assets/Images/directChatOn@2x.png differ diff --git a/Riot/Assets/Images/directChatOn@3x.png b/Riot/Assets/Images/directChatOn@3x.png index 0728f6337..4a33d1ea2 100755 Binary files a/Riot/Assets/Images/directChatOn@3x.png and b/Riot/Assets/Images/directChatOn@3x.png differ diff --git a/Riot/Assets/Images/jump_to_unread.png b/Riot/Assets/Images/jump_to_unread.png new file mode 100644 index 000000000..9a2e4bab3 Binary files /dev/null and b/Riot/Assets/Images/jump_to_unread.png differ diff --git a/Riot/Assets/Images/jump_to_unread@2x.png b/Riot/Assets/Images/jump_to_unread@2x.png new file mode 100644 index 000000000..336c3625b Binary files /dev/null and b/Riot/Assets/Images/jump_to_unread@2x.png differ diff --git a/Riot/Assets/Images/jump_to_unread@3x.png b/Riot/Assets/Images/jump_to_unread@3x.png new file mode 100644 index 000000000..decb05e85 Binary files /dev/null and b/Riot/Assets/Images/jump_to_unread@3x.png differ diff --git a/Riot/Assets/Images/tab_favourites.png b/Riot/Assets/Images/tab_favourites.png new file mode 100644 index 000000000..2615bcbea Binary files /dev/null and b/Riot/Assets/Images/tab_favourites.png differ diff --git a/Riot/Assets/Images/tab_favourites@2x.png b/Riot/Assets/Images/tab_favourites@2x.png new file mode 100644 index 000000000..7966d77e3 Binary files /dev/null and b/Riot/Assets/Images/tab_favourites@2x.png differ diff --git a/Riot/Assets/Images/tab_favourites@3x.png b/Riot/Assets/Images/tab_favourites@3x.png new file mode 100644 index 000000000..3182d590c Binary files /dev/null and b/Riot/Assets/Images/tab_favourites@3x.png differ diff --git a/Riot/Assets/Images/tab_favourites_selected.png b/Riot/Assets/Images/tab_favourites_selected.png new file mode 100644 index 000000000..44b7e5322 Binary files /dev/null and b/Riot/Assets/Images/tab_favourites_selected.png differ diff --git a/Riot/Assets/Images/tab_favourites_selected@2x.png b/Riot/Assets/Images/tab_favourites_selected@2x.png new file mode 100644 index 000000000..2dd4b84b4 Binary files /dev/null and b/Riot/Assets/Images/tab_favourites_selected@2x.png differ diff --git a/Riot/Assets/Images/tab_favourites_selected@3x.png b/Riot/Assets/Images/tab_favourites_selected@3x.png new file mode 100644 index 000000000..13da972cd Binary files /dev/null and b/Riot/Assets/Images/tab_favourites_selected@3x.png differ diff --git a/Riot/Assets/Images/tab_home.png b/Riot/Assets/Images/tab_home.png new file mode 100644 index 000000000..bddc3d750 Binary files /dev/null and b/Riot/Assets/Images/tab_home.png differ diff --git a/Riot/Assets/Images/tab_home@2x.png b/Riot/Assets/Images/tab_home@2x.png new file mode 100644 index 000000000..18268ca11 Binary files /dev/null and b/Riot/Assets/Images/tab_home@2x.png differ diff --git a/Riot/Assets/Images/tab_home@3x.png b/Riot/Assets/Images/tab_home@3x.png new file mode 100644 index 000000000..1421e7c70 Binary files /dev/null and b/Riot/Assets/Images/tab_home@3x.png differ diff --git a/Riot/Assets/Images/tab_home_selected.png b/Riot/Assets/Images/tab_home_selected.png new file mode 100644 index 000000000..5120728d7 Binary files /dev/null and b/Riot/Assets/Images/tab_home_selected.png differ diff --git a/Riot/Assets/Images/tab_home_selected@2x.png b/Riot/Assets/Images/tab_home_selected@2x.png new file mode 100644 index 000000000..96656c87b Binary files /dev/null and b/Riot/Assets/Images/tab_home_selected@2x.png differ diff --git a/Riot/Assets/Images/tab_home_selected@3x.png b/Riot/Assets/Images/tab_home_selected@3x.png new file mode 100644 index 000000000..4e6f4f664 Binary files /dev/null and b/Riot/Assets/Images/tab_home_selected@3x.png differ diff --git a/Riot/Assets/Images/tab_people.png b/Riot/Assets/Images/tab_people.png new file mode 100644 index 000000000..dd816af7f Binary files /dev/null and b/Riot/Assets/Images/tab_people.png differ diff --git a/Riot/Assets/Images/tab_people@2x.png b/Riot/Assets/Images/tab_people@2x.png new file mode 100644 index 000000000..75c7a7c96 Binary files /dev/null and b/Riot/Assets/Images/tab_people@2x.png differ diff --git a/Riot/Assets/Images/tab_people@3x.png b/Riot/Assets/Images/tab_people@3x.png new file mode 100644 index 000000000..9caaeede6 Binary files /dev/null and b/Riot/Assets/Images/tab_people@3x.png differ diff --git a/Riot/Assets/Images/tab_people_selected.png b/Riot/Assets/Images/tab_people_selected.png new file mode 100644 index 000000000..a688375e0 Binary files /dev/null and b/Riot/Assets/Images/tab_people_selected.png differ diff --git a/Riot/Assets/Images/tab_people_selected@2x.png b/Riot/Assets/Images/tab_people_selected@2x.png new file mode 100644 index 000000000..9f3131bfd Binary files /dev/null and b/Riot/Assets/Images/tab_people_selected@2x.png differ diff --git a/Riot/Assets/Images/tab_people_selected@3x.png b/Riot/Assets/Images/tab_people_selected@3x.png new file mode 100644 index 000000000..4fe7e28e5 Binary files /dev/null and b/Riot/Assets/Images/tab_people_selected@3x.png differ diff --git a/Riot/Assets/Images/tab_rooms.png b/Riot/Assets/Images/tab_rooms.png new file mode 100644 index 000000000..17ef20e73 Binary files /dev/null and b/Riot/Assets/Images/tab_rooms.png differ diff --git a/Riot/Assets/Images/tab_rooms@2x.png b/Riot/Assets/Images/tab_rooms@2x.png new file mode 100644 index 000000000..88b52c7b0 Binary files /dev/null and b/Riot/Assets/Images/tab_rooms@2x.png differ diff --git a/Riot/Assets/Images/tab_rooms@3x.png b/Riot/Assets/Images/tab_rooms@3x.png new file mode 100644 index 000000000..a42bc86ed Binary files /dev/null and b/Riot/Assets/Images/tab_rooms@3x.png differ diff --git a/Riot/Assets/Images/tab_rooms_selected.png b/Riot/Assets/Images/tab_rooms_selected.png new file mode 100644 index 000000000..7a431be22 Binary files /dev/null and b/Riot/Assets/Images/tab_rooms_selected.png differ diff --git a/Riot/Assets/Images/tab_rooms_selected@2x.png b/Riot/Assets/Images/tab_rooms_selected@2x.png new file mode 100644 index 000000000..5a928afeb Binary files /dev/null and b/Riot/Assets/Images/tab_rooms_selected@2x.png differ diff --git a/Riot/Assets/Images/tab_rooms_selected@3x.png b/Riot/Assets/Images/tab_rooms_selected@3x.png new file mode 100644 index 000000000..7dc4232ae Binary files /dev/null and b/Riot/Assets/Images/tab_rooms_selected@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 90e3367bb..0fb6e97be 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -16,7 +16,10 @@ */ // Titles -"title_recents" = "Messages"; +"title_home" = "Home"; +"title_favourites" = "Favourites"; +"title_people" = "People"; +"title_rooms" = "Rooms"; "warning" = "Warning"; // Actions @@ -113,21 +116,36 @@ "room_creation_invite_another_user" = "Search / invite by User ID, Name or email"; // Room recents -"room_recents_directory" = "DIRECTORY"; -"room_recents_favourites" = "FAVOURITES"; -"room_recents_conversations" = "ROOMS"; -"room_recents_low_priority" = "LOW PRIORITY"; -"room_recents_invites" = "INVITES"; +"room_recents_directory_section" = "ROOM DIRECTORY"; +"room_recents_directory_section_network" = "Network"; +"room_recents_favourites_section" = "FAVOURITES"; +"room_recents_people_section" = "PEOPLE"; +"room_recents_conversations_section" = "ROOMS"; +"room_recents_no_conversation" = "No rooms"; +"room_recents_low_priority_section" = "LOW PRIORITY"; +"room_recents_invites_section" = "INVITES"; "room_recents_start_chat_with" = "Start chat"; "room_recents_create_empty_room" = "Create room"; +"room_recents_join_room" = "Join room"; +"room_recents_join_room_title" = "Join a room"; +"room_recents_join_room_prompt" = "Type a room id or a room alias"; + +// People tab +"people_invites_section" = "INVITES"; +"people_conversation_section" = "CONVERSATIONS"; +"people_no_conversation" = "No conversations"; + +// Rooms tab +"room_directory_no_public_room" = "No public rooms available"; // Search "search_rooms" = "Rooms"; "search_messages" = "Messages"; "search_people" = "People"; "search_files" = "Files"; -"search_default_placeholder" = "Search..."; +"search_default_placeholder" = "Search"; "search_people_placeholder" = "Search by User ID, Name or email"; +"search_no_result" = "No results"; // Directory "directory_cell_title" = "Browse directory"; @@ -135,15 +153,16 @@ "directory_search_results_title" = "Browse directory results"; "directory_search_results" = "%tu results found for %@"; "directory_search_results_more_than" = ">%tu results found for %@"; -"directory_searching_title" = "Searching directory..."; +"directory_searching_title" = "Searching directory…"; "directory_search_fail" = "Failed to fetch data"; // Contacts -"contacts_address_book_section" = "LOCAL CONTACTS (%tu)"; +"contacts_address_book_section" = "LOCAL CONTACTS"; "contacts_address_book_matrix_users_toggle" = "Matrix users only"; -"contacts_matrix_users_section" = "KNOWN CONTACTS (%tu)"; -"contacts_matrix_users_default_section" = "KNOWN CONTACTS (-)"; -"contacts_matrix_users_search_prompt" = "Too many contacts, please use the search field"; +"contacts_address_book_no_contact" = "No local contacts"; +"contacts_address_book_permission_required" = "Permission required to access local contacts"; +"contacts_address_book_permission_denied" = "You didn't allow Riot to access your local contacts"; +"contacts_matrix_users_section" = "KNOWN CONTACTS"; // Chat participants "room_participants_title" = "Participants"; @@ -191,12 +210,15 @@ "room_participants_action_mention" = "Mention"; // Chat +"room_jump_to_first_unread" = "Jump to first unread message"; "room_new_message_notification" = "%d new message"; "room_new_messages_notification" = "%d new messages"; -"room_one_user_is_typing" = "%@ is typing..."; -"room_two_users_are_typing" = "%@ & %@ are typing..."; -"room_many_users_are_typing" = "%@, %@ & others are typing..."; -"room_message_placeholder" = "Type a message..."; +"room_one_user_is_typing" = "%@ is typing…"; +"room_two_users_are_typing" = "%@ & %@ are typing…"; +"room_many_users_are_typing" = "%@, %@ & others are typing…"; +"room_message_placeholder" = "Send a message (unencrypted)…"; +"encrypted_room_message_placeholder" = "Send an encrypted message…"; +"room_message_short_placeholder" = "Send a message…"; "room_offline_notification" = "Connectivity to the server has been lost."; "room_unsent_messages_notification" = "Messages not sent. %@ or %@ now?"; "room_unsent_messages_unknown_devices_notification" = "Message not sent due to unknown devices being present. %@ or %@ now?"; @@ -229,7 +251,7 @@ "unknown_devices_send_anyway" = "Send Anyway"; "unknown_devices_call_anyway" = "Call Anyway"; "unknown_devices_answer_anyway" = "Answer Anyway"; -"unknown_devices_verify" = "Verify..."; +"unknown_devices_verify" = "Verify…"; "unknown_devices_title" = "Unknown devices"; // Room Title @@ -253,6 +275,7 @@ "settings_config_no_build_info" = "No build info"; "settings_mark_all_as_read" = "Mark all messages as read"; +"settings_report_bug" = "Report bug"; "settings_clear_cache" = "Clear cache"; "settings_config_home_server" = "Home server is %@"; "settings_config_identity_server" = "Identity server is %@"; @@ -290,6 +313,8 @@ "settings_enable_push_notif" = "Notifications on this device"; "settings_global_settings_info" = "Global notification settings are available on your %@ web client"; +"settings_pin_rooms_with_missed_notif" = "Pin rooms with missed notifications"; +"settings_pin_rooms_with_unread" = "Pin rooms with unread messages"; "settings_on_denied_notification" = "Notifications are denied for %@, please allow them in your device settings"; //"settings_enable_all_notif" = "Enable all notifications"; //"settings_messages_my_display_name" = "Msg containing my display name"; @@ -399,6 +424,11 @@ // Directory "directory_title" = "Directory"; +"directory_server_picker_title" = "Select a directory"; +"directory_server_all_rooms" = "All rooms on %@ server"; +"directory_server_all_native_rooms" = "All native Matrix rooms"; +"directory_server_type_homeserver" = "Type a homeserver to list public rooms from"; +"directory_server_placeholder" = "matrix.org"; // Others "or" = "or"; @@ -410,6 +440,7 @@ "bug_report_prompt" = "The application has crashed last time. Would you like to submit a crash report?"; "rage_shake_prompt" = "You seem to be shaking the phone in frustration. Would you like to submit a bug report?"; "camera_access_not_granted" = "%@ doesn't have permission to use Camera, please change privacy settings"; +"large_badge_value_k_format" = "%.1fK"; // room display name "room_displayname_invite_from" = "Invite from %@"; @@ -433,3 +464,13 @@ "e2e_enabling_on_app_update" = "Riot now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings."; "e2e_need_log_in_again" = "You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver.\nThis is a once off; sorry for the inconvenience."; +// Bug report +"bug_report_title" = "Bug Report"; +"bug_report_description" = "Please describe the bug. What did you do? What did you expect to happen? What actually happened?"; +"bug_crash_report_title" = "Crash Report"; +"bug_crash_report_description" = "Please describe what you did before the crash:"; +"bug_report_logs_description" = "In order to diagnose problems, logs from this client will be sent with this bug report. If you would prefer to only send the text above, please untick:"; +"bug_report_send_logs" = "Send logs"; +"bug_report_send_screenshot" = "Send screenshot"; +"bug_report_progress_zipping" = "Collecting logs"; +"bug_report_progress_uploading" = "Uploading report"; diff --git a/Riot/Base.lproj/Main.storyboard b/Riot/Base.lproj/Main.storyboard index 69d4e5105..d4a2d4641 100644 --- a/Riot/Base.lproj/Main.storyboard +++ b/Riot/Base.lproj/Main.storyboard @@ -1,34 +1,34 @@ - + - + - + - + - + - + @@ -49,7 +49,7 @@ - + @@ -67,7 +67,7 @@ - + @@ -155,6 +155,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -171,7 +311,7 @@ - + @@ -189,7 +329,7 @@ - + @@ -204,7 +344,41 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -222,60 +396,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -286,12 +407,67 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -309,7 +485,7 @@ - + @@ -328,7 +504,7 @@ - + @@ -348,14 +524,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + diff --git a/Riot/Info.plist b/Riot/Info.plist index b0a9a351d..67d83c41e 100644 --- a/Riot/Info.plist +++ b/Riot/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.3.13 + 0.4.0 CFBundleSignature ???? CFBundleVersion - 0.3.13 + 0.4.0 ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode diff --git a/Riot/Model/Contact/ContactsDataSource.h b/Riot/Model/Contact/ContactsDataSource.h new file mode 100644 index 000000000..b63f74514 --- /dev/null +++ b/Riot/Model/Contact/ContactsDataSource.h @@ -0,0 +1,148 @@ +/* + Copyright 2017 Vector Creations 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 + +/** + 'ContactsDataSource' is a base class to handle contacts in Riot. + */ +@interface ContactsDataSource : MXKDataSource +{ +@protected + // Section indexes + NSInteger searchInputSection; + NSInteger filteredLocalContactsSection; + NSInteger filteredMatrixContactsSection; + + // Tell whether the non-matrix-enabled contacts must be hidden or not. NO by default. + BOOL hideNonMatrixEnabledContacts; + + // Search results + NSString *currentSearchText; + NSMutableArray *filteredLocalContacts; + NSMutableArray *filteredMatrixContacts; +} + +/** + Get the contact at the given index path. + + @param indexPath the index of the cell + @return the contact + */ +-(MXKContact *)contactAtIndexPath:(NSIndexPath*)indexPath; + +/** + Get the index path of the cell related to the provided contact. + + @param contact the contact. + @return indexPath the index of the cell (nil if not found or if the related section is shrinked). + */ +- (NSIndexPath*)cellIndexPathWithContact:(MXKContact*)contact; + +/** + Get the height of the section header view. + + @param section the section index + @return the header height. + */ +- (CGFloat)heightForHeaderInSection:(NSInteger)section; + +/** + Get the attributed string for the header title of the specified section. + + @param section the section index. + @return the section title. + */ +- (NSAttributedString *)attributedStringForHeaderTitleInSection:(NSInteger)section; + +/** + Get the section header view. + + @param section the section index + @param frame the drawing area for the header of the specified section. + @return the section header. + */ +- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame; + +/** + Get the sticky header view for the specified section. + + @param section the section index + @param frame the drawing area for the header of the specified section. + @return the sticky header view. + */ +- (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame; + +/** + Refresh the contacts data source and notify its delegate. + */ +- (void)forceRefresh; + +#pragma mark - Configuration +/** + Tell whether the sections are shrinkable. NO by default. + */ +@property (nonatomic) BOOL areSectionsShrinkable; + +/** + Tell whether the matrix id should be added by default in the matrix contact display name (NO by default). + If NO, the matrix id is added only to disambiguate the contact display names which appear several times. + */ +@property (nonatomic) BOOL forceMatrixIdInDisplayName; + +/** + The type of standard accessory view the contact cells should use + Default is UITableViewCellAccessoryNone. + */ +@property (nonatomic) UITableViewCellAccessoryType contactCellAccessoryType; + +/** + An image used to create a custom accessy view on the right side of the contact cells. + If set, use custom view. ignore accessoryType + */ +@property (nonatomic) UIImage *contactCellAccessoryImage; + +/** + The dictionary of the ignored local contacts, the keys are their email. Empty by default. + */ +@property (nonatomic) NSMutableDictionary *ignoredContactsByEmail; + +/** + The dictionary of the ignored matrix contacts, the keys are their matrix identifier. Empty by default. + */ +@property (nonatomic) NSMutableDictionary *ignoredContactsByMatrixId; + +/** + Filter the contacts list, by keeping only the contacts who have the search pattern + as prefix in their display name, their matrix identifiers and/or their contact methods (emails, phones). + + @param searchText the search pattern (nil to reset filtering). + @param forceReset tell whether the search request must be applied by ignoring the previous search result if any (use NO by default). + */ +- (void)searchWithPattern:(NSString *)searchText forceReset:(BOOL)forceReset; + +/** + Tell whether the search input is displayed in the contacts list. So that the user can select it (NO by default). + */ +@property (nonatomic) BOOL displaySearchInputInContactsList; + +/** + The temporary contact built from the search input. This contact is not nil only when the search input is + a valid email or a Matrix user ID. + */ +@property (nonatomic, readonly) MXKContact *searchInputContact; + +@end diff --git a/Riot/Model/Contact/ContactsDataSource.m b/Riot/Model/Contact/ContactsDataSource.m new file mode 100644 index 000000000..de0179948 --- /dev/null +++ b/Riot/Model/Contact/ContactsDataSource.m @@ -0,0 +1,998 @@ +/* + Copyright 2017 Vector Creations 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 "ContactsDataSource.h" +#import "ContactTableViewCell.h" + +#import "RiotDesignValues.h" + +#define CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE 0x01 +#define CONTACTSDATASOURCE_KNOWNCONTACTS_BITWISE 0x02 + +#define CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT 30.0 +#define CONTACTSDATASOURCE_LOCALCONTACTS_SECTION_HEADER_HEIGHT 65.0 + +@interface ContactsDataSource () +{ + // Search processing + dispatch_queue_t searchProcessingQueue; + NSUInteger searchProcessingCount; + NSString *searchProcessingText; + NSMutableArray *searchProcessingLocalContacts; + NSMutableArray *searchProcessingMatrixContacts; + + BOOL forceSearchResultRefresh; + + // This dictionary tells for each display name whether it appears several times. + NSMutableDictionary *isMultiUseNameByDisplayName; + + // Shrinked sections. + NSInteger shrinkedSectionsBitMask; + + UIView *localContactsCheckboxContainer; + UIImageView *localContactsCheckbox; +} + +@end + +@implementation ContactsDataSource + +- (instancetype)init +{ + self = [super init]; + if (self) + { + // Prepare search session + searchProcessingQueue = dispatch_queue_create("ContactsDataSource", DISPATCH_QUEUE_SERIAL); + searchProcessingCount = 0; + searchProcessingText = nil; + searchProcessingLocalContacts = nil; + searchProcessingMatrixContacts = nil; + + _ignoredContactsByEmail = [NSMutableDictionary dictionary]; + _ignoredContactsByMatrixId = [NSMutableDictionary dictionary]; + + isMultiUseNameByDisplayName = [NSMutableDictionary dictionary]; + + _forceMatrixIdInDisplayName = NO; + + _areSectionsShrinkable = NO; + shrinkedSectionsBitMask = 0; + + hideNonMatrixEnabledContacts = NO; + + _displaySearchInputInContactsList = NO; + + // Register on contact update notifications + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil]; + + // Refresh the matrix identifiers for all the local contacts. + if (ABAddressBookGetAuthorizationStatus() != kABAuthorizationStatusNotDetermined) + { + // Refresh the matrix identifiers for all the local contacts. + [[MXKContactManager sharedManager] updateMatrixIDsForAllLocalContacts]; + } + } + return self; +} + +- (void)destroy +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil]; + + filteredLocalContacts = nil; + filteredMatrixContacts = nil; + + _ignoredContactsByEmail = nil; + _ignoredContactsByMatrixId = nil; + + forceSearchResultRefresh = NO; + + searchProcessingQueue = nil; + searchProcessingLocalContacts = nil; + searchProcessingMatrixContacts = nil; + + isMultiUseNameByDisplayName = nil; + + _contactCellAccessoryImage = nil; + + localContactsCheckboxContainer = nil; + localContactsCheckbox = nil; + + [super destroy]; +} + +#pragma mark - + +- (void)forceRefresh +{ + // Check whether a search is in progress + if (searchProcessingCount) + { + forceSearchResultRefresh = YES; + return; + } + + // Refresh the search result + [self searchWithPattern:currentSearchText forceReset:YES]; +} + +- (void)setForceMatrixIdInDisplayName:(BOOL)forceMatrixIdInDisplayName +{ + if (_forceMatrixIdInDisplayName != forceMatrixIdInDisplayName) + { + _forceMatrixIdInDisplayName = forceMatrixIdInDisplayName; + + [self forceRefresh]; + } +} + +- (void)searchWithPattern:(NSString *)searchText forceReset:(BOOL)forceRefresh +{ + // Update search results. + searchText = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + searchProcessingCount++; + + if (!searchText.length) + { + searchProcessingLocalContacts = nil; + searchProcessingMatrixContacts = nil; + + // Disclose by default the sections if a search was in progress. + if (searchProcessingText.length) + { + shrinkedSectionsBitMask = 0; + } + } + else if (forceRefresh || !searchProcessingText.length || [searchText hasPrefix:searchProcessingText] == NO) + { + // Retrieve all the local contacts + searchProcessingLocalContacts = [self unfilteredLocalContactsArray]; + + // Retrieve all known matrix users + searchProcessingMatrixContacts = [self unfilteredMatrixContactsArray]; + + // Disclose the sections + shrinkedSectionsBitMask = 0; + } + + dispatch_async(searchProcessingQueue, ^{ + + for (NSUInteger index = 0; index < searchProcessingLocalContacts.count;) + { + MXKContact* contact = searchProcessingLocalContacts[index]; + + if (![contact hasPrefix:searchText]) + { + [searchProcessingLocalContacts removeObjectAtIndex:index]; + } + else + { + // Next + index++; + } + } + + for (NSUInteger index = 0; index < searchProcessingMatrixContacts.count;) + { + MXKContact* contact = searchProcessingMatrixContacts[index]; + + if (![contact hasPrefix:searchText]) + { + [searchProcessingMatrixContacts removeObjectAtIndex:index]; + } + else + { + // Next + index++; + } + } + + // Sort the refreshed list of the invitable contacts + [[MXKContactManager sharedManager] sortAlphabeticallyContacts:searchProcessingLocalContacts]; + [[MXKContactManager sharedManager] sortContactsByLastActiveInformation:searchProcessingMatrixContacts]; + + searchProcessingText = searchText; + + dispatch_sync(dispatch_get_main_queue(), ^{ + + // Sanity check: check whether self has been destroyed. + if (!searchProcessingQueue) + { + return; + } + + // Render the search result only if there is no other search in progress. + searchProcessingCount --; + + if (!searchProcessingCount) + { + if (!forceSearchResultRefresh) + { + // Update the filtered contacts. + currentSearchText = searchProcessingText; + filteredLocalContacts = searchProcessingLocalContacts; + filteredMatrixContacts = searchProcessingMatrixContacts; + + if (!self.forceMatrixIdInDisplayName) + { + [isMultiUseNameByDisplayName removeAllObjects]; + for (MXKContact* contact in filteredMatrixContacts) + { + isMultiUseNameByDisplayName[contact.displayName] = (isMultiUseNameByDisplayName[contact.displayName] ? @(YES) : @(NO)); + } + } + + // And inform the delegate about the update + [self.delegate dataSource:self didCellChange:nil]; + } + else + { + // Launch a new search + forceSearchResultRefresh = NO; + [self searchWithPattern:searchProcessingText forceReset:YES]; + } + } + }); + + }); +} + +- (void)setDisplaySearchInputInContactsList:(BOOL)displaySearchInputInContactsList +{ + if (_displaySearchInputInContactsList != displaySearchInputInContactsList) + { + _displaySearchInputInContactsList = displaySearchInputInContactsList; + + [self forceRefresh]; + } +} + +- (MXKContact*)searchInputContact +{ + // Check whether the current search input is a valid email or a Matrix user ID + if (currentSearchText.length && ([MXTools isEmailAddress:currentSearchText] || [MXTools isMatrixUserIdentifier:currentSearchText])) + { + return [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; + } + + return nil; +} + +#pragma mark - Internals + +- (void)onContactManagerDidUpdate:(NSNotification *)notif +{ + [self forceRefresh]; +} + +- (NSMutableArray*)unfilteredLocalContactsArray +{ + // Retrieve all the contacts obtained by splitting each local contact by contact method. This list is ordered alphabetically. + NSMutableArray *unfilteredLocalContacts = [NSMutableArray arrayWithArray:[MXKContactManager sharedManager].localContactsSplitByContactMethod]; + + // Remove the ignored contacts + // + Check whether the non-matrix-enabled contacts must be ignored + for (NSUInteger index = 0; index < unfilteredLocalContacts.count;) + { + MXKContact* contact = unfilteredLocalContacts[index]; + + NSArray *identifiers = contact.matrixIdentifiers; + if (identifiers.count) + { + if ([_ignoredContactsByMatrixId objectForKey:identifiers.firstObject]) + { + [unfilteredLocalContacts removeObjectAtIndex:index]; + continue; + } + } + else if (hideNonMatrixEnabledContacts) + { + // Ignore non-matrix-enabled contact + [unfilteredLocalContacts removeObjectAtIndex:index]; + continue; + } + else + { + NSArray *emails = contact.emailAddresses; + if (emails.count) + { + // Here the contact has only one email address. + MXKEmail *email = emails.firstObject; + + // Trick: ignore @facebook.com email addresses from the results - facebook have discontinued that service... + if ([_ignoredContactsByEmail objectForKey:email.emailAddress] || [email.emailAddress hasSuffix:@"@facebook.com"]) + { + [unfilteredLocalContacts removeObjectAtIndex:index]; + continue; + } + } + else + { + // The contact has here a phone number. + // Ignore this contact if the phone number is not linked to a matrix id because the invitation by SMS is not supported yet. + MXKPhoneNumber *phoneNumber = contact.phoneNumbers.firstObject; + if (!phoneNumber.matrixID) + { + [unfilteredLocalContacts removeObjectAtIndex:index]; + continue; + } + } + } + + index++; + } + + return unfilteredLocalContacts; +} + +- (NSMutableArray*)unfilteredMatrixContactsArray +{ + NSArray *matrixContacts = [MXKContactManager sharedManager].matrixContacts; + NSMutableArray *unfilteredMatrixContacts = [NSMutableArray arrayWithCapacity:matrixContacts.count]; + + // Matrix ids: split contacts with several ids, and remove the current participants. + for (MXKContact* contact in matrixContacts) + { + NSArray *identifiers = contact.matrixIdentifiers; + if (identifiers.count > 1) + { + for (NSString *userId in identifiers) + { + if ([_ignoredContactsByMatrixId objectForKey:userId] == nil) + { + MXKContact *splitContact = [[MXKContact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; + [unfilteredMatrixContacts addObject:splitContact]; + } + } + } + else if (identifiers.count) + { + NSString *userId = identifiers.firstObject; + if ([_ignoredContactsByMatrixId objectForKey:userId] == nil) + { + [unfilteredMatrixContacts addObject:contact]; + } + } + } + + return unfilteredMatrixContacts; +} + +#pragma mark - UITableView data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + NSInteger count = 0; + + searchInputSection = filteredLocalContactsSection = filteredMatrixContactsSection = -1; + + if (currentSearchText.length) + { + if (_displaySearchInputInContactsList) + { + searchInputSection = count++; + } + + // Keep visible the header for the both contact sections, even if their are empty. + filteredLocalContactsSection = count++; + filteredMatrixContactsSection = count++; + } + else + { + // Display by default the full address book ordered alphabetically, mixing Matrix enabled and non-Matrix enabled users. + if (!filteredLocalContacts) + { + filteredLocalContacts = [self unfilteredLocalContactsArray]; + } + + // Keep visible the local contact header, even if the section is empty. + filteredLocalContactsSection = count++; + } + + + + return count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + NSInteger count = 0; + + if (section == searchInputSection) + { + count = 1; + } + else if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) + { + // Display a default cell when no local contacts is available. + count = filteredLocalContacts.count ? filteredLocalContacts.count : 1; + } + else if (section == filteredMatrixContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_KNOWNCONTACTS_BITWISE)) + { + // Display a default cell when no contacts is available. + count = filteredMatrixContacts.count ? filteredMatrixContacts.count : 1; + } + + return count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Prepare a contact cell here + MXKContact *contact; + BOOL showMatrixIdInDisplayName = NO; + + if (indexPath.section == searchInputSection) + { + // Show what the user is typing in a cell. So that he can click on it + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; + } + else if (indexPath.section == filteredLocalContactsSection) + { + if (indexPath.row < filteredLocalContacts.count) + { + contact = filteredLocalContacts[indexPath.row]; + showMatrixIdInDisplayName = YES; + } + } + else if (indexPath.section == filteredMatrixContactsSection) + { + if (indexPath.row < filteredMatrixContacts.count) + { + contact = filteredMatrixContacts[indexPath.row]; + + showMatrixIdInDisplayName = self.forceMatrixIdInDisplayName ? YES : [isMultiUseNameByDisplayName[contact.displayName] isEqualToNumber:@(YES)]; + } + } + + if (contact) + { + ContactTableViewCell *contactCell = [tableView dequeueReusableCellWithIdentifier:[ContactTableViewCell defaultReuseIdentifier]]; + if (!contactCell) + { + contactCell = [[ContactTableViewCell alloc] init]; + } + + // Make the cell display the contact + [contactCell render:contact]; + + contactCell.selectionStyle = UITableViewCellSelectionStyleDefault; + contactCell.showMatrixIdInDisplayName = showMatrixIdInDisplayName; + + // The search displays contacts to invite. + if (indexPath.section == filteredLocalContactsSection || indexPath.section == filteredMatrixContactsSection) + { + // Add the right accessory view if any + contactCell.accessoryType = self.contactCellAccessoryType; + if (self.contactCellAccessoryImage) + { + contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage]; + } + + } + else if (indexPath.section == searchInputSection) + { + // This is the text entered by the user + // Check whether the search input is a valid email or a Matrix user ID before adding the accessory view. + if (![MXTools isEmailAddress:currentSearchText] && ![MXTools isMatrixUserIdentifier:currentSearchText]) + { + contactCell.contentView.alpha = 0.5; + contactCell.userInteractionEnabled = NO; + } + else + { + // Add the right accessory view if any + contactCell.accessoryType = self.contactCellAccessoryType; + if (self.contactCellAccessoryImage) + { + contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage]; + } + } + } + + return contactCell; + } + else + { + MXKTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCell defaultReuseIdentifier]]; + if (!tableViewCell) + { + tableViewCell = [[MXKTableViewCell alloc] init]; + tableViewCell.textLabel.textColor = kRiotTextColorGray; + tableViewCell.textLabel.font = [UIFont systemFontOfSize:15.0]; + tableViewCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + // Check whether a search session is in progress + if (currentSearchText.length) + { + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"search_no_result", @"Vector", nil); + } + else if (indexPath.section == filteredLocalContactsSection) + { + tableViewCell.textLabel.numberOfLines = 0; + + // Indicate to the user why there is no contacts + switch (ABAddressBookGetAuthorizationStatus()) + { + case kABAuthorizationStatusAuthorized: + // Because there is no contacts on the device + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"contacts_address_book_no_contact", @"Vector", nil); + break; + + case kABAuthorizationStatusNotDetermined: + // Because the user have not granted the permission yet + // (The permission request popup is displayed at the same time) + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"contacts_address_book_permission_required", @"Vector", nil); + break; + + default: + { + // Because the user didn't allow the app to access local contacts + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"contacts_address_book_permission_denied", @"Vector", nil); + break; + } + } + } + return tableViewCell; + } + + return nil; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return NO; +} + +#pragma mark - + +-(MXKContact *)contactAtIndexPath:(NSIndexPath*)indexPath +{ + NSInteger row = indexPath.row; + MXKContact *mxkContact; + + if (indexPath.section == searchInputSection) + { + mxkContact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; + } + else if (indexPath.section == filteredLocalContactsSection && row < filteredLocalContacts.count) + { + mxkContact = filteredLocalContacts[row]; + } + else if (indexPath.section == filteredMatrixContactsSection && row < filteredMatrixContacts.count) + { + mxkContact = filteredMatrixContacts[row]; + } + + return mxkContact; +} + +- (NSIndexPath*)cellIndexPathWithContact:(MXKContact*)contact +{ + NSIndexPath *indexPath = nil; + + NSUInteger index = [filteredLocalContacts indexOfObject:contact]; + if (index != NSNotFound) + { + indexPath = [NSIndexPath indexPathForRow:index inSection:filteredLocalContactsSection]; + } + else + { + index = [filteredMatrixContacts indexOfObject:contact]; + if (index != NSNotFound) + { + indexPath = [NSIndexPath indexPathForRow:index inSection:filteredMatrixContactsSection]; + } + } + return indexPath; +} + +- (CGFloat)heightForHeaderInSection:(NSInteger)section +{ + if (section == filteredLocalContactsSection || section == filteredMatrixContactsSection) + { + if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) + { + return CONTACTSDATASOURCE_LOCALCONTACTS_SECTION_HEADER_HEIGHT; + } + + return CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; + } + return 0; +} + +- (NSAttributedString *)attributedStringForHeaderTitleInSection:(NSInteger)section +{ + NSAttributedString *sectionTitle; + NSString* title; + NSUInteger count = 0; + + if (section == filteredLocalContactsSection) + { + count = filteredLocalContacts.count; + title = NSLocalizedStringFromTable(@"contacts_address_book_section", @"Vector", nil); + } + else //if (section == filteredMatrixContactsSection) + { + title = NSLocalizedStringFromTable(@"contacts_matrix_users_section", @"Vector", nil); + + if (currentSearchText.length) + { + count = filteredMatrixContacts.count; + } + } + + if (count) + { + NSString *roomCount = [NSString stringWithFormat:@" %tu", count]; + + NSMutableAttributedString *mutableSectionTitle = [[NSMutableAttributedString alloc] initWithString:title + attributes:@{NSForegroundColorAttributeName : kRiotTextColorBlack, + NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]; + [mutableSectionTitle appendAttributedString:[[NSMutableAttributedString alloc] initWithString:roomCount + attributes:@{NSForegroundColorAttributeName : kRiotColorSilver, + NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]]; + + sectionTitle = mutableSectionTitle; + } + else if (title) + { + sectionTitle = [[NSAttributedString alloc] initWithString:title + attributes:@{NSForegroundColorAttributeName : kRiotTextColorBlack, + NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]; + } + + return sectionTitle; +} + +- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame +{ + UIView* sectionHeader; + + NSInteger sectionBitwise = 0; + + sectionHeader = [[UIView alloc] initWithFrame:frame]; + sectionHeader.backgroundColor = kRiotColorLightGrey; + + frame.origin.x = 20; + frame.origin.y = 5; + frame.size.width = sectionHeader.frame.size.width - 10; + frame.size.height = CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT -10; + UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame]; + headerLabel.attributedText = [self attributedStringForHeaderTitleInSection:section]; + headerLabel.backgroundColor = [UIColor clearColor]; + [sectionHeader addSubview:headerLabel]; + + if (_areSectionsShrinkable) + { + if (section == filteredLocalContactsSection) + { + sectionBitwise = CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE; + } + else //if (section == filteredMatrixContactsSection) + { + if (currentSearchText.length) + { + // This section is collapsable only if it is not empty + if (filteredMatrixContacts.count) + { + sectionBitwise = CONTACTSDATASOURCE_KNOWNCONTACTS_BITWISE; + } + } + } + } + + if (sectionBitwise) + { + // Add shrink button + UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom]; + frame = sectionHeader.frame; + frame.origin.x = frame.origin.y = 0; + frame.size.height = CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; + shrinkButton.frame = frame; + shrinkButton.backgroundColor = [UIColor clearColor]; + [shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + shrinkButton.tag = sectionBitwise; + [sectionHeader addSubview:shrinkButton]; + sectionHeader.userInteractionEnabled = YES; + + // Add shrink icon + UIImage *chevron; + if (shrinkedSectionsBitMask & sectionBitwise) + { + chevron = [UIImage imageNamed:@"disclosure_icon"]; + } + else + { + chevron = [UIImage imageNamed:@"shrink_icon"]; + } + UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron]; + chevronView.contentMode = UIViewContentModeCenter; + frame = chevronView.frame; + frame.origin.x = shrinkButton.frame.size.width - frame.size.width - 16; + frame.origin.y = (shrinkButton.frame.size.height - frame.size.height) / 2; + chevronView.frame = frame; + [sectionHeader addSubview:chevronView]; + chevronView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin); + } + + if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE)) + { + NSLayoutConstraint *leadingConstraint, *trailingConstraint, *topConstraint, *bottomConstraint; + NSLayoutConstraint *widthConstraint, *heightConstraint, *centerYConstraint; + + if (!localContactsCheckboxContainer) + { + CGFloat containerWidth = sectionHeader.frame.size.width; + + localContactsCheckboxContainer = [[UIView alloc] initWithFrame:CGRectMake(0, CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT, containerWidth, sectionHeader.frame.size.height - CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT)]; + localContactsCheckboxContainer.backgroundColor = [UIColor clearColor]; + localContactsCheckboxContainer.translatesAutoresizingMaskIntoConstraints = NO; + + // Add Checkbox and Label + localContactsCheckbox = [[UIImageView alloc] initWithFrame:CGRectMake(23, 5, 22, 22)]; + localContactsCheckbox.translatesAutoresizingMaskIntoConstraints = NO; + [localContactsCheckboxContainer addSubview:localContactsCheckbox]; + + UILabel *checkboxLabel = [[UILabel alloc] initWithFrame:CGRectMake(54, 5, containerWidth - 64, 30)]; + checkboxLabel.translatesAutoresizingMaskIntoConstraints = NO; + checkboxLabel.textColor = kRiotTextColorBlack; + checkboxLabel.font = [UIFont systemFontOfSize:16.0]; + checkboxLabel.text = NSLocalizedStringFromTable(@"contacts_address_book_matrix_users_toggle", @"Vector", nil); + [localContactsCheckboxContainer addSubview:checkboxLabel]; + + UIView *checkboxMask = [[UIView alloc] initWithFrame:CGRectMake(16, -2, 36, 36)]; + checkboxMask.translatesAutoresizingMaskIntoConstraints = NO; + [localContactsCheckboxContainer addSubview:checkboxMask]; + // Listen to check box tap + checkboxMask.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCheckBoxTap:)]; + [tapGesture setNumberOfTouchesRequired:1]; + [tapGesture setNumberOfTapsRequired:1]; + [tapGesture setDelegate:self]; + [checkboxMask addGestureRecognizer:tapGesture]; + + // Add switch constraints + leadingConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:23]; + + topConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeTop + multiplier:1 + constant:5]; + + widthConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:22]; + heightConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:22]; + + [NSLayoutConstraint activateConstraints:@[leadingConstraint, topConstraint, widthConstraint, heightConstraint]]; + + + // Add Label constraints + centerYConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckbox + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0.0f]; + heightConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:30]; + leadingConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckbox + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:10]; + trailingConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:-10]; + + [NSLayoutConstraint activateConstraints:@[centerYConstraint, heightConstraint, leadingConstraint, trailingConstraint]]; + + // Add check box mask constraints + heightConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:36]; + + centerYConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckbox + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0.0f]; + + leadingConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:localContactsCheckbox + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:-7]; + + trailingConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:checkboxLabel + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:0]; + + [NSLayoutConstraint activateConstraints:@[heightConstraint, centerYConstraint, leadingConstraint, trailingConstraint]]; + } + + // Set the right value of the tick box + localContactsCheckbox.image = hideNonMatrixEnabledContacts ? [UIImage imageNamed:@"selection_tick"] : [UIImage imageNamed:@"selection_untick"]; + + // Add the check box container + [sectionHeader addSubview:localContactsCheckboxContainer]; + leadingConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:0]; + widthConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeWidth + multiplier:1 + constant:0]; + topConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeTop + multiplier:1 + constant:CONTACTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT]; + bottomConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:0]; + + [NSLayoutConstraint activateConstraints:@[leadingConstraint, widthConstraint, topConstraint, bottomConstraint]]; + } + + return sectionHeader; +} + +- (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame +{ + // Return the section header used when the section is shrinked + NSInteger savedShrinkedSectionsBitMask = shrinkedSectionsBitMask; + shrinkedSectionsBitMask = CONTACTSDATASOURCE_LOCALCONTACTS_BITWISE | CONTACTSDATASOURCE_KNOWNCONTACTS_BITWISE; + + UIView *stickyHeader = [self viewForHeaderInSection:section withFrame:frame]; + + shrinkedSectionsBitMask = savedShrinkedSectionsBitMask; + + return stickyHeader; +} + +#pragma mark - Action + +- (IBAction)onButtonPressed:(id)sender +{ + if ([sender isKindOfClass:[UIButton class]]) + { + UIButton *shrinkButton = (UIButton*)sender; + NSInteger selectedSectionBit = shrinkButton.tag; + + if (shrinkedSectionsBitMask & selectedSectionBit) + { + // Disclose the section + shrinkedSectionsBitMask &= ~selectedSectionBit; + } + else + { + // Shrink this section + shrinkedSectionsBitMask |= selectedSectionBit; + } + + // Inform the delegate about the update + [self.delegate dataSource:self didCellChange:nil]; + } +} + +#pragma mark - Action + +- (IBAction)onCheckBoxTap:(UITapGestureRecognizer*)sender +{ + // Update local contacts filter + hideNonMatrixEnabledContacts = !hideNonMatrixEnabledContacts; + + // Check whether a search is in progress + if (searchProcessingCount) + { + forceSearchResultRefresh = YES; + return; + } + + // Refresh the search result + if (hideNonMatrixEnabledContacts) + { + // Remove the non-matrix-enabled contacts from the current filtered local contacts + for (NSUInteger index = 0; index < filteredLocalContacts.count;) + { + MXKContact* contact = filteredLocalContacts[index]; + + NSArray *identifiers = contact.matrixIdentifiers; + if (!identifiers.count) + { + [filteredLocalContacts removeObjectAtIndex:index]; + continue; + } + + index++; + } + + // Refresh display + [self.delegate dataSource:self didCellChange:nil]; + } + else + { + // Refresh the search result by launching a new search session. + [self searchWithPattern:currentSearchText forceReset:YES]; + } +} + +@end diff --git a/Riot/Model/Room/RoomBubbleCellData.h b/Riot/Model/Room/RoomBubbleCellData.h index 48dfa5e6b..a59d3103d 100644 --- a/Riot/Model/Room/RoomBubbleCellData.h +++ b/Riot/Model/Room/RoomBubbleCellData.h @@ -38,7 +38,7 @@ @property(nonatomic) NSString *selectedEventId; /** - The index of the most recent component (component with timestamp). NSNotFound by default. + The index of the most recent component (component with a timestamp, and an actual display). NSNotFound by default. */ @property(nonatomic, readonly) NSInteger mostRecentComponentIndex; diff --git a/Riot/Model/Room/RoomBubbleCellData.m b/Riot/Model/Room/RoomBubbleCellData.m index a953f2248..36388b619 100644 --- a/Riot/Model/Room/RoomBubbleCellData.m +++ b/Riot/Model/Room/RoomBubbleCellData.m @@ -33,9 +33,6 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; if (self) { - // Use the vector style placeholder - self.senderAvatarPlaceholder = [AvatarGenerator generateAvatarForMatrixItem:self.senderId withDisplayName:self.senderDisplayName]; - // Increase maximum number of components self.maxComponentCount = 20; @@ -112,99 +109,92 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; // Refresh the receipt flag during this process _hasReadReceipts = NO; - MXKRoomBubbleComponent *component = [bubbleComponents firstObject]; - NSAttributedString *componentString = component.attributedTextMessage; - -#ifndef DEBUG - // Sanity check: we observed some app crashes due to a nil string in a component. - // According to the implementation this case should not happen because the components are removed as soon as their string is nil. - // We patch here this issue by adding some logs in order to investigate it in the future. - if (!componentString) - { - NSLog(@"[RoomBubbleCellData] WARNING: refreshAttributedTextMessage: unexpected empty component (0/%tu), %@", bubbleComponents.count, component.event.eventId); - componentString = [[NSAttributedString alloc] initWithString:@""]; - } -#endif - NSInteger selectedComponentIndex = self.selectedComponentIndex; NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; - // Check whether another component than the first one is selected - // Note: When a component is selected, it is highlighted by applying an alpha on other components. - if (selectedComponentIndex != NSNotFound && selectedComponentIndex != 0) + MXKRoomBubbleComponent *component; + NSAttributedString *componentString; + NSUInteger index = 0; + for (; index < bubbleComponents.count; index++) { - // Apply alpha to blur this component - NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; - UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; - color = [color colorWithAlphaComponent:0.2]; - - [customComponentString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; - componentString = customComponentString; - } - - // Check whether the timestamp is displayed for this first component, and check whether a vertical whitespace is required - if ((selectedComponentIndex == 0 || lastMessageIndex == 0) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) - { - currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; - [currentAttributedTextMsg appendAttributedString:componentString]; - } - else - { - // Init attributed string with the first text component - currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; - } - - // Vertical whitespace is added in case of read receipts - if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) - { - _hasReadReceipts = YES; - [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } - - for (NSUInteger index = 1; index < bubbleComponents.count; index++) - { - [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; - component = bubbleComponents[index]; componentString = component.attributedTextMessage; -#ifndef DEBUG - // Sanity check: we observed some app crashes due to a nil string in a component. - // According to the implementation this case should not happen because the components are removed as soon as their string is nil. - // We patch here this issue by adding some logs in order to investigate it in the future. - if (!componentString) + if (componentString) { - NSLog(@"[RoomBubbleCellData] refreshAttributedTextMessage: WARNING: unexpected empty component (%tu/%tu), %@", index, bubbleComponents.count, component.event.eventId); - componentString = [[NSAttributedString alloc] initWithString:@""]; - } -#endif - - // Check whether another component than this one is selected - if (selectedComponentIndex != NSNotFound && selectedComponentIndex != index) - { - // Apply alpha to blur this component - NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; - UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; - color = [color colorWithAlphaComponent:0.2]; + // Check whether another component than this one is selected + // Note: When a component is selected, it is highlighted by applying an alpha on other components. + if (selectedComponentIndex != NSNotFound && selectedComponentIndex != index) + { + // Apply alpha to blur this component + NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; + UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; + color = [color colorWithAlphaComponent:0.2]; + + [customComponentString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; + componentString = customComponentString; + } - [customComponentString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; - componentString = customComponentString; + // Check whether the timestamp is displayed for this component, and check whether a vertical whitespace is required + if ((selectedComponentIndex == index || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + { + currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; + [currentAttributedTextMsg appendAttributedString:componentString]; + } + else + { + // Init attributed string with the first text component + currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; + } + + // Vertical whitespace is added in case of read receipts + if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) + { + _hasReadReceipts = YES; + [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } + + // The first non empty component has been handled. + break; } + } + + for (index++; index < bubbleComponents.count; index++) + { + component = bubbleComponents[index]; + componentString = component.attributedTextMessage; - // Check whether the timestamp is displayed - if (selectedComponentIndex == index || lastMessageIndex == index) + if (componentString) { - [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; - } - - // Append attributed text - [currentAttributedTextMsg appendAttributedString:componentString]; - - // Add vertical whitespace in case of read receipts - if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) - { - _hasReadReceipts = YES; - [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; + + // Check whether another component than this one is selected + if (selectedComponentIndex != NSNotFound && selectedComponentIndex != index) + { + // Apply alpha to blur this component + NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; + UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; + color = [color colorWithAlphaComponent:0.2]; + + [customComponentString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; + componentString = customComponentString; + } + + // Check whether the timestamp is displayed + if (selectedComponentIndex == index || lastMessageIndex == index) + { + [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; + } + + // Append attributed text + [currentAttributedTextMsg appendAttributedString:componentString]; + + // Add vertical whitespace in case of read receipts + if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) + { + _hasReadReceipts = YES; + [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } } } @@ -224,22 +214,32 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; if (bubbleComponents.count) { // Set position of the first component - MXKRoomBubbleComponent *component = [bubbleComponents firstObject]; - CGFloat positionY = (self.attachment == nil || self.attachment.type == MXKAttachmentTypeFile) ? MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET : 0; - component.position = CGPointMake(0, positionY); - - _hasReadReceipts = ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO] != nil); + MXKRoomBubbleComponent *component; + NSUInteger index = 0; + for (; index < bubbleComponents.count; index++) + { + // Compute the vertical position for next component + component = [bubbleComponents objectAtIndex:index]; + + component.position = CGPointMake(0, positionY); + + if (component.attributedTextMessage) + { + _hasReadReceipts = ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO] != nil); + break; + } + } // Check whether the position of other components need to be refreshed - if (!self.attachment && bubbleComponents.count > 1) + if (!self.attachment && index < bubbleComponents.count) { NSMutableAttributedString *attributedString; NSInteger selectedComponentIndex = self.selectedComponentIndex; NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; // Check whether the timestamp is displayed for this first component, and check whether a vertical whitespace is required - if ((selectedComponentIndex == 0 || lastMessageIndex == 0) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + if ((selectedComponentIndex == index || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) { attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; [attributedString appendAttributedString:component.attributedTextMessage]; @@ -258,44 +258,51 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; - for (NSUInteger index = 1; index < bubbleComponents.count; index++) + for (index++; index < bubbleComponents.count; index++) { // Compute the vertical position for next component component = [bubbleComponents objectAtIndex:index]; - // Prepare its attributed string by considering potential vertical margin required to display timestamp. - NSAttributedString *componentString; - if (selectedComponentIndex == index || lastMessageIndex == index) + if (component.attributedTextMessage) { - NSMutableAttributedString *componentAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; - [componentAttributedString appendAttributedString:component.attributedTextMessage]; + // Prepare its attributed string by considering potential vertical margin required to display timestamp. + NSAttributedString *componentString; + if (selectedComponentIndex == index || lastMessageIndex == index) + { + NSMutableAttributedString *componentAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; + [componentAttributedString appendAttributedString:component.attributedTextMessage]; + + componentString = componentAttributedString; + } + else + { + componentString = component.attributedTextMessage; + } - componentString = componentAttributedString; + // Append this attributed string. + [attributedString appendAttributedString:componentString]; + + // Compute the height of the resulting string. + CGFloat cumulatedHeight = [self rawTextHeight:attributedString]; + + // Deduce the position of the beginning of this component. + positionY = MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET + (cumulatedHeight - [self rawTextHeight:componentString]); + + component.position = CGPointMake(0, positionY); + + // Add vertical whitespace in case of read receipts. + if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) + { + _hasReadReceipts = YES; + [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } + + [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; } else { - componentString = component.attributedTextMessage; + component.position = CGPointMake(0, positionY); } - - // Append this attributed string. - [attributedString appendAttributedString:componentString]; - - // Compute the height of the resulting string. - CGFloat cumulatedHeight = [self rawTextHeight:attributedString]; - - // Deduce the position of the beginning of this component. - CGFloat positionY = MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET + (cumulatedHeight - [self rawTextHeight:componentString]); - - component.position = CGPointMake(0, positionY); - - // Add vertical whitespace in case of read receipts. - if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) - { - _hasReadReceipts = YES; - [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } - - [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; } } } @@ -351,7 +358,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; while (index--) { MXKRoomBubbleComponent *component = components[index]; - if (component.date) + if (component.attributedTextMessage && component.date) { mostRecentComponentIndex = index; break; diff --git a/Riot/Model/Room/RoomDataSource.h b/Riot/Model/Room/RoomDataSource.h index 1640c679a..bed1979e3 100644 --- a/Riot/Model/Room/RoomDataSource.h +++ b/Riot/Model/Room/RoomDataSource.h @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket Ltd + Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,4 +27,9 @@ */ @property(nonatomic) NSString *selectedEventId; +/** + Tell whether the initial event of the timeline (if any) must be marked. Default is NO. + */ +@property(nonatomic) BOOL markTimelineInitialEvent; + @end diff --git a/Riot/Model/Room/RoomDataSource.m b/Riot/Model/Room/RoomDataSource.m index dd63e3829..4a1a71d77 100644 --- a/Riot/Model/Room/RoomDataSource.m +++ b/Riot/Model/Room/RoomDataSource.m @@ -22,6 +22,7 @@ #import "MXKRoomBubbleTableViewCell+Riot.h" #import "AvatarGenerator.h" +#import "RiotDesignValues.h" #import "MXRoom+Riot.h" @@ -40,16 +41,19 @@ self.eventFormatter.treatMatrixUserIdAsLink = YES; self.eventFormatter.treatMatrixRoomIdAsLink = YES; self.eventFormatter.treatMatrixRoomAliasAsLink = YES; + + // Apply the event types filter to display only the wanted event types. + self.eventFormatter.eventTypesFilterForMessages = [MXKAppSettings standardAppSettings].eventsFilterForMessages; // Handle timestamp and read receips display at Vector app level (see [tableView: cellForRowAtIndexPath:]) self.useCustomDateTimeLabel = YES; self.useCustomReceipts = YES; self.useCustomUnsentButton = YES; - // TODO custom here self.eventsFilterForMessages according to Vector requirements - // Set bubble pagination self.bubblesPagination = MXKRoomDataSourceBubblesPaginationPerDay; + + self.markTimelineInitialEvent = NO; } return self; } @@ -69,11 +73,10 @@ for (NSString* eventId in readEventIds) { RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId]; - cellData.hasReadReceipts = YES; + // Ignore the read receipts on the events without an actual display. + cellData.hasReadReceipts = !cellData.hasNoDisplay; } - - // Let super handle this receipt [super didReceiveReceiptEvent:receiptEvent roomState:roomState]; } @@ -94,9 +97,17 @@ cellData.containsLastMessage = NO; } - // The cell containing the last message is the last one - RoomBubbleCellData *cellData = bubbles.lastObject; - cellData.containsLastMessage = YES; + // The cell containing the last message is the last one with an actual display. + NSInteger index = bubbles.count; + while (index--) + { + RoomBubbleCellData *cellData = bubbles[index]; + if (cellData.attributedTextMessage) + { + cellData.containsLastMessage = YES; + break; + } + } } } @@ -105,6 +116,15 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + // Do cell data customization that needs to be done before [MXKRoomBubbleTableViewCell render] + RoomBubbleCellData *roomBubbleCellData = [self cellDataAtIndex:indexPath.row]; + + // Use the Riot style placeholder + if (!roomBubbleCellData.senderAvatarPlaceholder) + { + roomBubbleCellData.senderAvatarPlaceholder = [AvatarGenerator generateAvatarForMatrixItem:roomBubbleCellData.senderId withDisplayName:roomBubbleCellData.senderDisplayName]; + } + UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; // Finalize cell view customization here @@ -112,6 +132,7 @@ { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; RoomBubbleCellData *cellData = (RoomBubbleCellData*)bubbleCell.bubbleData; + NSArray *bubbleComponents = cellData.bubbleComponents; // Display timestamp of the last message if (cellData.containsLastMessage) @@ -119,8 +140,9 @@ [bubbleCell addTimestampLabelForComponent:cellData.mostRecentComponentIndex]; } - // Handle read receipts display. - if (cellData.hasReadReceipts && self.showBubbleReceipts) + // Handle read receipts and read marker display. + // Ignore the read receipts on the bubble without actual display. + if ((self.showBubbleReceipts && cellData.hasReadReceipts) || self.showReadMarker) { // Read receipts container are inserted here on the right side into the overlay container. // Some vertical whitespaces are added in message text view (see RoomBubbleCellData class) to insert correctly multiple receipts. @@ -129,7 +151,6 @@ bubbleCell.bubbleOverlayContainer.userInteractionEnabled = NO; bubbleCell.bubbleOverlayContainer.hidden = NO; - NSArray *bubbleComponents = cellData.bubbleComponents; NSInteger index = bubbleComponents.count; CGFloat bottomPositionY = bubbleCell.frame.size.height; while (index--) @@ -138,82 +159,136 @@ if (component.event.sentState != MXEventSentStateFailed) { - // Get the events receipts by ignoring the current user receipt. - NSArray* receipts = [self.room getEventReceipts:component.event.eventId sorted:YES]; - NSMutableArray *roomMembers; - NSMutableArray *placeholders; - - // Check whether some receipts are found - if (receipts.count) + // Handle read receipts (if any) + if (self.showBubbleReceipts && cellData.hasReadReceipts) { - // Retrieve the corresponding room members - roomMembers = [[NSMutableArray alloc] initWithCapacity:receipts.count]; - placeholders = [[NSMutableArray alloc] initWithCapacity:receipts.count]; + // Get the events receipts by ignoring the current user receipt. + NSArray* receipts = [self.room getEventReceipts:component.event.eventId sorted:YES]; + NSMutableArray *roomMembers; + NSMutableArray *placeholders; - for (MXReceiptData* data in receipts) + // Check whether some receipts are found + if (receipts.count) { - MXRoomMember * roomMember = [self.room.state memberWithUserId:data.userId]; - if (roomMember) + // Retrieve the corresponding room members + roomMembers = [[NSMutableArray alloc] initWithCapacity:receipts.count]; + placeholders = [[NSMutableArray alloc] initWithCapacity:receipts.count]; + + for (MXReceiptData* data in receipts) { - [roomMembers addObject:roomMember]; - [placeholders addObject:[AvatarGenerator generateAvatarForMatrixItem:roomMember.userId withDisplayName:roomMember.displayname]]; + MXRoomMember * roomMember = [self.room.state memberWithUserId:data.userId]; + if (roomMember) + { + [roomMembers addObject:roomMember]; + [placeholders addObject:[AvatarGenerator generateAvatarForMatrixItem:roomMember.userId withDisplayName:roomMember.displayname]]; + } } } + + // Check whether some receipts are found + if (roomMembers.count) + { + // Define the read receipts container, positioned on the right border of the bubble cell (Note the right margin 6 pts). + MXKReceiptSendersContainer* avatarsContainer = [[MXKReceiptSendersContainer alloc] initWithFrame:CGRectMake(bubbleCell.frame.size.width - 156, bottomPositionY - 13, 150, 12) andRestClient:self.mxSession.matrixRestClient]; + + // Custom avatar display + avatarsContainer.maxDisplayedAvatars = 5; + avatarsContainer.avatarMargin = 6; + + // Set the container tag to be able to retrieve read receipts container from component index (see component selection in MXKRoomBubbleTableViewCell (Vector) category). + avatarsContainer.tag = index; + + [avatarsContainer refreshReceiptSenders:roomMembers withPlaceHolders:placeholders andAlignment:ReadReceiptAlignmentRight]; + + avatarsContainer.translatesAutoresizingMaskIntoConstraints = NO; + avatarsContainer.accessibilityIdentifier = @"readReceiptsContainer"; + [bubbleCell.bubbleOverlayContainer addSubview:avatarsContainer]; + + // Force receipts container size + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:150]; + NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:12]; + + // Force receipts container position + NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:-6]; + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:bottomPositionY - 13]; + + // Available on iOS 8 and later + [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, topConstraint, trailingConstraint]]; + } } - // Check whether some receipts are found - if (roomMembers.count) + // Check whether the read marker must be displayed here. + if (self.showReadMarker) { - // Define the read receipts container, positioned on the right border of the bubble cell (Note the right margin 6 pts). - MXKReceiptSendersContainer* avatarsContainer = [[MXKReceiptSendersContainer alloc] initWithFrame:CGRectMake(bubbleCell.frame.size.width - 156, bottomPositionY - 13, 150, 12) andRestClient:self.mxSession.matrixRestClient]; - - // Custom avatar display - avatarsContainer.maxDisplayedAvatars = 5; - avatarsContainer.avatarMargin = 6; - - // Set the container tag to be able to retrieve read receipts container from component index (see component selection in MXKRoomBubbleTableViewCell (Vector) category). - avatarsContainer.tag = index; - - [avatarsContainer refreshReceiptSenders:roomMembers withPlaceHolders:placeholders andAlignment:ReadReceiptAlignmentRight]; - - avatarsContainer.translatesAutoresizingMaskIntoConstraints = NO; - avatarsContainer.accessibilityIdentifier = @"readReceiptsContainer"; - [bubbleCell.bubbleOverlayContainer addSubview:avatarsContainer]; - - // Force receipts container size - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:150]; - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:12]; - - // Force receipts container position - NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.bubbleOverlayContainer - attribute:NSLayoutAttributeTrailing - multiplier:1.0 - constant:-6]; - NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.bubbleOverlayContainer - attribute:NSLayoutAttributeTop - multiplier:1.0 - constant:bottomPositionY - 13]; - - // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, topConstraint, trailingConstraint]]; + if ([component.event.eventId isEqualToString:self.room.accountData.readMarkerEventId]) + { + bubbleCell.readMarkerView = [[UIView alloc] initWithFrame:CGRectMake(0, bottomPositionY - 2, bubbleCell.bubbleOverlayContainer.frame.size.width, 2)]; + bubbleCell.readMarkerView.backgroundColor = kRiotColorGreen; + // Hide by default the marker, it will be shown and animated when the cell will be rendered. + bubbleCell.readMarkerView.hidden = YES; + bubbleCell.readMarkerView.tag = index; + + bubbleCell.readMarkerView.translatesAutoresizingMaskIntoConstraints = NO; + bubbleCell.readMarkerView.accessibilityIdentifier = @"readMarker"; + [bubbleCell.bubbleOverlayContainer addSubview:bubbleCell.readMarkerView]; + + // Force read marker constraints + bubbleCell.readMarkerViewTopConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:bottomPositionY - 2]; + bubbleCell.readMarkerViewLeadingConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0]; + bubbleCell.readMarkerViewTrailingConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.readMarkerView + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0]; + + bubbleCell.readMarkerViewHeightConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:2]; + + [NSLayoutConstraint activateConstraints:@[bubbleCell.readMarkerViewTopConstraint, bubbleCell.readMarkerViewLeadingConstraint, bubbleCell.readMarkerViewTrailingConstraint, bubbleCell.readMarkerViewHeightConstraint]]; + } } } @@ -244,10 +319,8 @@ } // Manage initial event (case of permalink or search result) - if (self.timeline.initialEventId) + if (self.timeline.initialEventId && self.markTimelineInitialEvent) { - NSArray *bubbleComponents = cellData.bubbleComponents; - // Check if the cell contains this initial event for (NSUInteger index = 0; index < bubbleComponents.count; index++) { diff --git a/Riot/Model/Room/RoomPreviewData.m b/Riot/Model/Room/RoomPreviewData.m index d42f38e43..e03cae846 100644 --- a/Riot/Model/Room/RoomPreviewData.m +++ b/Riot/Model/Room/RoomPreviewData.m @@ -58,6 +58,12 @@ _roomTopic = publicRoom.topic; _roomAliases = publicRoom.aliases; _numJoinedMembers = publicRoom.numJoinedMembers; + + if (!_roomName.length) + { + // Consider the room aliases to define a default room name. + _roomName = _roomAliases.firstObject; + } } return self; } @@ -80,6 +86,7 @@ // Create the room data source _roomDataSource = [[RoomDataSource alloc] initWithPeekingRoom:peekingRoom andInitialEventId:_eventId]; [_roomDataSource finalizeInitialization]; + _roomDataSource.markTimelineInitialEvent = YES; _roomName = peekingRoom.riotDisplayname; _roomAvatarUrl = peekingRoom.state.avatar; diff --git a/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.h b/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.h index 62aa79ada..bb9906a1d 100644 --- a/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.h +++ b/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.h @@ -33,6 +33,29 @@ */ @interface PublicRoomsDirectoryDataSource : MXKDataSource +/** + The homeserver to list public rooms from. + Default is nil. In this case, the user's homeserver is used. + */ +@property (nonatomic) NSString *homeserver; + +/** + Flag to indicate to list all public rooms from all networks of `homeserver`. + NO will list only pure Matrix rooms. + */ +@property (nonatomic) BOOL includeAllNetworks; + +/** + List public rooms from a third party protocol. + Default is nil. + */ +@property (nonatomic) MXThirdPartyProtocolInstance *thirdpartyProtocolInstance; + +/** + The display name of the current directory server. + */ +@property (nonatomic, readonly) NSString *directoryServerDisplayname; + /** The number of public rooms matching `searchPattern`. It is accurate only if 'moreThanRoomsCount' is NO. @@ -96,4 +119,12 @@ */ - (MXPublicRoom*)roomAtIndexPath:(NSIndexPath*)indexPath; +/** + Get the height of the cell at the given index path. + + @param indexPath the index of the cell + @return the cell height + */ +- (CGFloat)cellHeightAtIndexPath:(NSIndexPath*)indexPath; + @end diff --git a/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.m b/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.m index 15a475649..690996d0c 100644 --- a/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.m +++ b/Riot/Model/RoomList/PublicRoomsDirectoryDataSource.m @@ -59,6 +59,78 @@ double const kPublicRoomsDirectoryDataExpiration = 10; return self; } +- (NSString *)directoryServerDisplayname +{ + NSString *directoryServerDisplayname; + + if (_homeserver) + { + directoryServerDisplayname = _homeserver; + } + else if (_thirdpartyProtocolInstance) + { + directoryServerDisplayname = _thirdpartyProtocolInstance.desc; + } + else + { + if (_includeAllNetworks) + { + // We display all rooms, included bridged ones, of the user's HS + directoryServerDisplayname = self.mxSession.matrixRestClient.credentials.homeServerName; + } + else + { + // We display only Matrix rooms of the user's HS + directoryServerDisplayname = [NSBundle mxk_localizedStringForKey:@"matrix"]; + } + } + + return directoryServerDisplayname; +} + +- (void)setHomeserver:(NSString *)homeserver +{ + if ([homeserver isEqualToString:self.mxSession.matrixRestClient.credentials.homeServerName]) + { + // The CS API does not like we pass the user's HS as parameter + homeserver = nil; + } + + _thirdpartyProtocolInstance = nil; + + if (homeserver != _homeserver) + { + _homeserver = homeserver; + + // Reset data + [self resetPagination]; + } +} + +- (void)setIncludeAllNetworks:(BOOL)includeAllNetworks +{ + if (includeAllNetworks != _includeAllNetworks) + { + _includeAllNetworks = includeAllNetworks; + + // Reset data + [self resetPagination]; + } +} + +- (void)setThirdpartyProtocolInstance:(MXThirdPartyProtocolInstance *)thirdpartyProtocolInstance +{ + if (thirdpartyProtocolInstance != _thirdpartyProtocolInstance) + { + _homeserver = nil; + _includeAllNetworks = NO; + _thirdpartyProtocolInstance = thirdpartyProtocolInstance; + + // Reset data + [self resetPagination]; + } +} + - (void)setSearchPattern:(NSString *)searchPattern { if (searchPattern) @@ -66,7 +138,7 @@ double const kPublicRoomsDirectoryDataExpiration = 10; if (![searchPattern isEqualToString:_searchPattern]) { _searchPattern = searchPattern; - [self startPagination]; + [self resetPagination]; } } else @@ -76,7 +148,7 @@ double const kPublicRoomsDirectoryDataExpiration = 10; if (_searchPattern || rooms.count == 0) { _searchPattern = searchPattern; - [self startPagination]; + [self resetPagination]; } } } @@ -112,7 +184,17 @@ double const kPublicRoomsDirectoryDataExpiration = 10; return room; } -- (void)startPagination +- (CGFloat)cellHeightAtIndexPath:(NSIndexPath*)indexPath +{ + if (indexPath.row < rooms.count) + { + return PublicRoomTableViewCell.cellHeight; + } + + return 50.0; +} + +- (void)resetPagination { // Cancel the previous request if (publicRoomsRequest) @@ -120,17 +202,12 @@ double const kPublicRoomsDirectoryDataExpiration = 10; [publicRoomsRequest cancel]; } - [self setState:MXKDataSourceStatePreparing]; - // Reset all pagination vars [rooms removeAllObjects]; nextBatch = nil; _roomsCount = 0; _moreThanRoomsCount = NO; _hasReachedPaginationEnd = NO; - - // And do a single pagination - [self paginate:nil failure:nil]; } - (MXHTTPOperation *)paginate:(void (^)(NSUInteger))complete failure:(void (^)(NSError *))failure @@ -140,11 +217,13 @@ double const kPublicRoomsDirectoryDataExpiration = 10; return nil; } + [self setState:MXKDataSourceStatePreparing]; + __weak typeof(self) weakSelf = self; // Get the public rooms from the server MXHTTPOperation *newPublicRoomsRequest; - newPublicRoomsRequest = [self.mxSession.matrixRestClient publicRoomsOnServer:nil limit:_paginationLimit since:nextBatch filter:_searchPattern thirdPartyInstanceId:nil includeAllNetworks:NO success:^(MXPublicRoomsResponse *publicRoomsResponse) { + newPublicRoomsRequest = [self.mxSession.matrixRestClient publicRoomsOnServer:_homeserver limit:_paginationLimit since:nextBatch filter:_searchPattern thirdPartyInstanceId:_thirdpartyProtocolInstance.instanceId includeAllNetworks:_includeAllNetworks success:^(MXPublicRoomsResponse *publicRoomsResponse) { if (weakSelf) { @@ -231,21 +310,46 @@ double const kPublicRoomsDirectoryDataExpiration = 10; - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return rooms.count; + // Display a default cell when no rooms is available. + return rooms.count ? rooms.count : 1; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - // For now reuse MatrixKit cells - PublicRoomTableViewCell *publicRoomCell = [tableView dequeueReusableCellWithIdentifier:[PublicRoomTableViewCell defaultReuseIdentifier]]; - if (!publicRoomCell) + // Sanity check + if (indexPath.row < rooms.count) { - publicRoomCell = [[PublicRoomTableViewCell alloc] init]; + PublicRoomTableViewCell *publicRoomCell = [tableView dequeueReusableCellWithIdentifier:[PublicRoomTableViewCell defaultReuseIdentifier]]; + if (!publicRoomCell) + { + publicRoomCell = [[PublicRoomTableViewCell alloc] init]; + } + + [publicRoomCell render:rooms[indexPath.row] withMatrixSession:self.mxSession]; + return publicRoomCell; + } + else + { + MXKTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCell defaultReuseIdentifier]]; + if (!tableViewCell) + { + tableViewCell = [[MXKTableViewCell alloc] init]; + tableViewCell.textLabel.textColor = kRiotTextColorGray; + tableViewCell.textLabel.font = [UIFont systemFontOfSize:15.0]; + tableViewCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + if (_searchPattern.length) + { + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"search_no_result", @"Vector", nil); + } + else + { + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"room_directory_no_public_room", @"Vector", nil); + } + + return tableViewCell; } - - [publicRoomCell render:rooms[indexPath.row] withMatrixSession:self.mxSession]; - - return publicRoomCell; } @end diff --git a/Riot/Model/RoomList/RecentCellData.m b/Riot/Model/RoomList/RecentCellData.m index 278d0252f..b1c8623ca 100644 --- a/Riot/Model/RoomList/RecentCellData.m +++ b/Riot/Model/RoomList/RecentCellData.m @@ -24,6 +24,36 @@ // self.roomDisplayname returns this value instead of the mother class. @synthesize roomDisplayname; +- (NSString*)notificationCountStringValue +{ + NSString *stringValue; + NSUInteger notificationCount = self.notificationCount; + + if (notificationCount > 1000) + { + CGFloat value = notificationCount / 1000.0; + stringValue = [NSString stringWithFormat:NSLocalizedStringFromTable(@"large_badge_value_k_format", @"Vector", nil), value]; + } + else + { + stringValue = [NSString stringWithFormat:@"%tu", notificationCount]; + } + + return stringValue; +} + +- (NSUInteger)notificationCount +{ + // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. + if (self.roomSummary.room.isMentionsOnly) + { + // Only the highlighted missed messages must be considered here. + return self.roomSummary.highlightCount; + } + + return self.roomSummary.notificationCount; +} + - (void)update { [super update]; diff --git a/Riot/Model/RoomList/RecentsDataSource.h b/Riot/Model/RoomList/RecentsDataSource.h index 998cad0f3..81ca32c18 100644 --- a/Riot/Model/RoomList/RecentsDataSource.h +++ b/Riot/Model/RoomList/RecentsDataSource.h @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket Ltd + Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,34 +17,90 @@ #import -@class PublicRoomsDirectoryDataSource; +#import "PublicRoomsDirectoryDataSource.h" /** - 'RecentsDataSource' class inherits from 'MXKInterleavedRecentsDataSource' to define Riot recents source. + List the different modes used to prepare the recents data source. + Each mode corresponds to an application tab: Home, Favourites, People and Rooms. + */ +typedef enum : NSUInteger +{ + RecentsDataSourceModeHome, + RecentsDataSourceModeFavourites, + RecentsDataSourceModePeople, + RecentsDataSourceModeRooms + +} RecentsDataSourceMode; + + +/** + Action identifier used when the user tapped on the directory change button. + + The `userInfo` is nil. + */ +extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange; + +/** + 'RecentsDataSource' class inherits from 'MXKInterleavedRecentsDataSource' to define the Riot recents source + shared between all the applications tabs. */ @interface RecentsDataSource : MXKInterleavedRecentsDataSource +@property (nonatomic) NSInteger directorySection; +@property (nonatomic) NSInteger invitesSection; +@property (nonatomic) NSInteger favoritesSection; +@property (nonatomic) NSInteger peopleSection; +@property (nonatomic) NSInteger conversationSection; +@property (nonatomic) NSInteger lowPrioritySection; + +@property (nonatomic, readonly) NSArray* invitesCellDataArray; +@property (nonatomic, readonly) NSArray* favoriteCellDataArray; +@property (nonatomic, readonly) NSArray* peopleCellDataArray; +@property (nonatomic, readonly) NSArray* conversationCellDataArray; +@property (nonatomic, readonly) NSArray* lowPriorityCellDataArray; + /** - Return the header height from the section. + Set the delegate by specifying the selected display mode. + */ +- (void)setDelegate:(id)delegate andRecentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode; + +/** + The current mode (RecentsDataSourceModeHome by default). + */ +@property (nonatomic, readonly) RecentsDataSourceMode recentsDataSourceMode; + +/** + The data source used to manage the rooms from directory. + */ +@property (nonatomic) PublicRoomsDirectoryDataSource *publicRoomsDirectoryDataSource; + +/** + Refresh the recents data source and notify its delegate. + */ +- (void)forceRefresh; + +/** + Tell whether the sections are shrinkable. NO by default. + */ +@property (nonatomic) BOOL areSectionsShrinkable; + +/** + Get the sticky header view for the specified section. + + @param section the section index + @param frame the drawing area for the header of the specified section. + @return the sticky header view. + */ +- (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame; + +/** + Get the height of the section header view. + + @param section the section index + @return the header height. */ - (CGFloat)heightForHeaderInSection:(NSInteger)section; -#pragma mark - Directory handling -/** - The data source used to manage search in public rooms. - */ -@property (nonatomic, readonly) PublicRoomsDirectoryDataSource *publicRoomsDirectoryDataSource; - -/** - Hide the public rooms directory cell. YES by default. - */ -@property (nonatomic) BOOL hidePublicRoomsDirectory; - -/** - Hide recents. NO by default. - */ -@property (nonatomic) BOOL hideRecents; - #pragma mark - Drag & Drop handling /** Return true of the cell can be moved from a section to another one. @@ -78,4 +135,34 @@ */ - (void)moveRoomCell:(MXRoom*)room from:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath success:(void (^)())moveSuccess failure:(void (^)(NSError *error))moveFailure; +/** + The current number of the favourite rooms with missed notifications. + */ +@property (nonatomic, readonly) NSUInteger missedFavouriteDiscussionsCount; + +/** + The current number of the favourite rooms with unread highlighted messages. + */ +@property (nonatomic, readonly) NSUInteger missedHighlightFavouriteDiscussionsCount; + +/** + The current number of the direct chats with missed notifications, including the invites. + */ +@property (nonatomic, readonly) NSUInteger missedDirectDiscussionsCount; + +/** + The current number of the direct chats with unread highlighted messages. + */ +@property (nonatomic, readonly) NSUInteger missedHighlightDirectDiscussionsCount; + +/** + The current number of the group chats with missed notifications, including the invites. + */ +@property (nonatomic, readonly) NSUInteger missedGroupDiscussionsCount; + +/** + The current number of the group chats with unread highlighted messages. + */ +@property (nonatomic, readonly) NSUInteger missedHighlightGroupDiscussionsCount; + @end diff --git a/Riot/Model/RoomList/RecentsDataSource.m b/Riot/Model/RoomList/RecentsDataSource.m index b4ba172a9..334946390 100644 --- a/Riot/Model/RoomList/RecentsDataSource.m +++ b/Riot/Model/RoomList/RecentsDataSource.m @@ -17,54 +17,50 @@ #import "RecentsDataSource.h" -#import "EventFormatter.h" +#import "RecentCellData.h" #import "RiotDesignValues.h" -#import "RoomIdOrAliasTableViewCell.h" -#import "DirectoryRecentTableViewCell.h" - -#import "PublicRoomsDirectoryDataSource.h" - #import "MXRoom+Riot.h" -#import "RecentCellData.h" +#import "AppDelegate.h" #define RECENTSDATASOURCE_SECTION_DIRECTORY 0x01 #define RECENTSDATASOURCE_SECTION_INVITES 0x02 #define RECENTSDATASOURCE_SECTION_FAVORITES 0x04 #define RECENTSDATASOURCE_SECTION_CONVERSATIONS 0x08 #define RECENTSDATASOURCE_SECTION_LOWPRIORITY 0x10 +#define RECENTSDATASOURCE_SECTION_PEOPLE 0x20 + +#define RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT 30.0 +#define RECENTSDATASOURCE_DIRECTORY_SECTION_HEADER_HEIGHT 65.0 + +NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSourceTapOnDirectoryServerChange"; @interface RecentsDataSource() { NSMutableArray* invitesCellDataArray; NSMutableArray* favoriteCellDataArray; + NSMutableArray* peopleCellDataArray; NSMutableArray* conversationCellDataArray; NSMutableArray* lowPriorityCellDataArray; - - NSInteger searchedRoomIdOrAliasSection; // used to display the potential room id or alias typed during search. - NSInteger directorySection; - NSInteger invitesSection; - NSInteger favoritesSection; - NSInteger conversationSection; - NSInteger lowPrioritySection; - NSInteger sectionsCount; NSInteger shrinkedSectionsBitMask; - + + UIView *directorySectionContainer; + UILabel *directoryServerLabel; + NSMutableDictionary *roomTagsListenerByUserId; - // The potential room id or alias typed in search input. - NSString *roomIdOrAlias; - // Timer to not refresh publicRoomsDirectoryDataSource on every keystroke. NSTimer *publicRoomsTriggerTimer; } @end @implementation RecentsDataSource +@synthesize directorySection, invitesSection, favoritesSection, peopleSection, conversationSection, lowPrioritySection; @synthesize hiddenCellIndexPath, droppingCellIndexPath, droppingCellBackGroundView; +@synthesize invitesCellDataArray, favoriteCellDataArray, peopleCellDataArray, conversationCellDataArray, lowPriorityCellDataArray; - (instancetype)init { @@ -73,20 +69,18 @@ { invitesCellDataArray = [[NSMutableArray alloc] init]; favoriteCellDataArray = [[NSMutableArray alloc] init]; + peopleCellDataArray = [[NSMutableArray alloc] init]; lowPriorityCellDataArray = [[NSMutableArray alloc] init]; conversationCellDataArray = [[NSMutableArray alloc] init]; - searchedRoomIdOrAliasSection = -1; directorySection = -1; invitesSection = -1; favoritesSection = -1; + peopleSection = -1; conversationSection = -1; lowPrioritySection = -1; - sectionsCount = 0; - - _hideRecents = NO; - _hidePublicRoomsDirectory = YES; + _areSectionsShrinkable = NO; shrinkedSectionsBitMask = 0; roomTagsListenerByUserId = [[NSMutableDictionary alloc] init]; @@ -97,6 +91,41 @@ return self; } +#pragma mark - + +- (void)setDelegate:(id)delegate andRecentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode +{ + // Update the configuration, the recentsDataSourceMode setter will force a refresh. + self.delegate = delegate; + self.recentsDataSourceMode = recentsDataSourceMode; +} + +- (void)setRecentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode +{ + _recentsDataSourceMode = recentsDataSourceMode; + + [self forceRefresh]; +} + +- (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame +{ + UIView *stickyHeader; + + NSInteger savedShrinkedSectionsBitMask = shrinkedSectionsBitMask; + if (section == directorySection) + { + // Return the section header used when the section is shrinked + shrinkedSectionsBitMask = RECENTSDATASOURCE_SECTION_DIRECTORY; + } + + stickyHeader = [self viewForHeaderInSection:section withFrame:frame]; + + shrinkedSectionsBitMask = savedShrinkedSectionsBitMask; + + return stickyHeader; +} + +#pragma mark - - (MXKSessionRecentsDataSource *)addMatrixSession:(MXSession *)mxSession { @@ -140,7 +169,11 @@ { if (dataSource == _publicRoomsDirectoryDataSource) { - [self refreshRoomsSectionsAndReload]; + if (-1 != directorySection && !self.droppingCellIndexPath) + { + // TODO: We should only update the directory section + [self.delegate dataSource:self didCellChange:nil]; + } } else { @@ -157,7 +190,7 @@ { dispatch_async(dispatch_get_main_queue(), ^{ - [self refreshRoomsSectionsAndReload]; + [self forceRefresh]; }); } @@ -169,7 +202,7 @@ } } -- (void)refreshRoomsSectionsAndReload +- (void)forceRefresh { // Refresh is disabled during drag&drop animation" if (!self.droppingCellIndexPath) @@ -186,93 +219,90 @@ MXSession *mxSession = notif.object; if ([self.mxSessions indexOfObject:mxSession] != NSNotFound) { - [self refreshRoomsSectionsAndReload]; - } -} - -#pragma mark - - -- (void)setHidePublicRoomsDirectory:(BOOL)hidePublicRoomsDirectory -{ - if (_hidePublicRoomsDirectory != hidePublicRoomsDirectory) - { - _hidePublicRoomsDirectory = hidePublicRoomsDirectory; - - if (!_hidePublicRoomsDirectory) - { - // Start by looking for all public rooms - self.publicRoomsDirectoryDataSource.searchPattern = nil; - } - - [self refreshRoomsSectionsAndReload]; - } -} - -- (void)setHideRecents:(BOOL)hideRecents -{ - if (_hideRecents != hideRecents) - { - _hideRecents = hideRecents; - - [self refreshRoomsSectionsAndReload]; + [self forceRefresh]; } } #pragma mark - UITableViewDataSource -/** - Return the header height from the section. - */ -- (CGFloat)heightForHeaderInSection:(NSInteger)section -{ - if ((section == directorySection) || (section == invitesSection) || (section == favoritesSection) || (section == conversationSection) || (section == lowPrioritySection)) - { - return 30.0f; - } - - return 0.0f; -} - - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + // Sanity check + if (tableView.tag != self.recentsDataSourceMode) + { + // The view controller of this table view is not the current selected one in the tab bar controller. + return 0; + } + + NSInteger sectionsCount = 0; + // Check whether all data sources are ready before rendering recents if (self.state == MXKDataSourceStateReady) { - // Return the last updated number of sections. - return sectionsCount; + directorySection = favoritesSection = peopleSection = conversationSection = lowPrioritySection = invitesSection = -1; + + if (invitesCellDataArray.count > 0) + { + invitesSection = sectionsCount++; + } + + if (favoriteCellDataArray.count > 0) + { + favoritesSection = sectionsCount++; + } + + if (_recentsDataSourceMode == RecentsDataSourceModeHome) + { + peopleSection = sectionsCount++; + } + + // Keep visible the main rooms section even if it is empty, except on favourites screen. + if (_recentsDataSourceMode != RecentsDataSourceModeFavourites) + { + conversationSection = sectionsCount++; + } + + if (_recentsDataSourceMode == RecentsDataSourceModeRooms) + { + // Add the directory section after "ROOMS" + directorySection = sectionsCount++; + } + + if (lowPriorityCellDataArray.count > 0) + { + lowPrioritySection = sectionsCount++; + } } - return 0; -} - -- (BOOL)isMovingCellSection:(NSInteger)section -{ - return self.droppingCellIndexPath && (self.droppingCellIndexPath.section == section); -} - -- (BOOL)isHiddenCellSection:(NSInteger)section -{ - return self.hiddenCellIndexPath && (self.hiddenCellIndexPath.section == section); + + return sectionsCount; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + // Sanity check + if (tableView.tag != self.recentsDataSourceMode) + { + // The view controller of this table view is not the current selected one in the tab bar controller. + return 0; + } + NSUInteger count = 0; - if (section == searchedRoomIdOrAliasSection) - { - count = 1; - } - else if (section == directorySection) - { - count = 1; - } - else if (section == favoritesSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_FAVORITES)) + if (section == favoritesSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_FAVORITES)) { count = favoriteCellDataArray.count; } + else if (section == peopleSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_PEOPLE)) + { + count = peopleCellDataArray.count ? peopleCellDataArray.count : 1; + } else if (section == conversationSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_CONVERSATIONS)) { - count = conversationCellDataArray.count; + count = conversationCellDataArray.count ? conversationCellDataArray.count : 1; + } + else if (section == directorySection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY)) + { + count = [_publicRoomsDirectoryDataSource tableView:tableView numberOfRowsInSection:0]; } else if (section == lowPrioritySection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_LOWPRIORITY)) { @@ -283,6 +313,7 @@ count = invitesCellDataArray.count; } + // Adjust this count according to the potential dragged cell. if ([self isMovingCellSection:section]) { count++; @@ -296,127 +327,465 @@ return count; } +- (CGFloat)heightForHeaderInSection:(NSInteger)section +{ + if (section == directorySection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY)) + { + return RECENTSDATASOURCE_DIRECTORY_SECTION_HEADER_HEIGHT; + } + + return RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; +} + +- (NSAttributedString *)attributedStringForHeaderTitleInSection:(NSInteger)section +{ + NSAttributedString *sectionTitle; + NSString *title; + NSUInteger count = 0; + + if (section == favoritesSection) + { + count = favoriteCellDataArray.count; + title = NSLocalizedStringFromTable(@"room_recents_favourites_section", @"Vector", nil); + } + else if (section == peopleSection) + { + count = peopleCellDataArray.count; + title = NSLocalizedStringFromTable(@"room_recents_people_section", @"Vector", nil); + } + else if (section == conversationSection) + { + count = conversationCellDataArray.count; + + if (_recentsDataSourceMode == RecentsDataSourceModePeople) + { + title = NSLocalizedStringFromTable(@"people_conversation_section", @"Vector", nil); + } + else + { + title = NSLocalizedStringFromTable(@"room_recents_conversations_section", @"Vector", nil); + } + } + else if (section == directorySection) + { + title = NSLocalizedStringFromTable(@"room_recents_directory_section", @"Vector", nil); + } + else if (section == lowPrioritySection) + { + count = lowPriorityCellDataArray.count; + title = NSLocalizedStringFromTable(@"room_recents_low_priority_section", @"Vector", nil); + } + else if (section == invitesSection) + { + count = invitesCellDataArray.count; + + if (_recentsDataSourceMode == RecentsDataSourceModePeople) + { + title = NSLocalizedStringFromTable(@"people_invites_section", @"Vector", nil); + } + else + { + title = NSLocalizedStringFromTable(@"room_recents_invites_section", @"Vector", nil); + } + } + + if (count) + { + NSString *roomCount = [NSString stringWithFormat:@" %tu", count]; + + NSMutableAttributedString *mutableSectionTitle = [[NSMutableAttributedString alloc] initWithString:title + attributes:@{NSForegroundColorAttributeName : kRiotTextColorBlack, + NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]; + [mutableSectionTitle appendAttributedString:[[NSMutableAttributedString alloc] initWithString:roomCount + attributes:@{NSForegroundColorAttributeName : kRiotColorSilver, + NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]]; + + sectionTitle = mutableSectionTitle; + } + else if (title) + { + sectionTitle = [[NSAttributedString alloc] initWithString:title + attributes:@{NSForegroundColorAttributeName : kRiotTextColorBlack, + NSFontAttributeName: [UIFont boldSystemFontOfSize:15.0]}]; + } + + return sectionTitle; +} + +- (UIView *)badgeViewForHeaderTitleInHomeSection:(NSInteger)section +{ + // Prepare a badge to display the total of missed notifications in this section. + NSUInteger count = 0; + NSArray *sectionArray; + UIView *missedNotifAndUnreadBadgeBgView = nil; + + if (section == favoritesSection) + { + sectionArray = favoriteCellDataArray; + } + else if (section == peopleSection) + { + sectionArray = peopleCellDataArray; + } + else if (section == conversationSection) + { + sectionArray = conversationCellDataArray; + } + else if (section == lowPrioritySection) + { + sectionArray = lowPriorityCellDataArray; + } + + for (id cellData in sectionArray) + { + count += cellData.notificationCount; + } + + if (count) + { + UILabel *missedNotifAndUnreadBadgeLabel = [[UILabel alloc] init]; + missedNotifAndUnreadBadgeLabel.textColor = [UIColor whiteColor]; + missedNotifAndUnreadBadgeLabel.font = [UIFont boldSystemFontOfSize:14]; + if (count > 1000) + { + CGFloat value = count / 1000.0; + missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"large_badge_value_k_format", @"Vector", nil), value]; + } + else + { + missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", count]; + } + + [missedNotifAndUnreadBadgeLabel sizeToFit]; + + CGFloat bgViewWidth = missedNotifAndUnreadBadgeLabel.frame.size.width + 18; + + missedNotifAndUnreadBadgeBgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, bgViewWidth, 20)]; + [missedNotifAndUnreadBadgeBgView.layer setCornerRadius:10]; + missedNotifAndUnreadBadgeBgView.backgroundColor = kRiotColorSilver; + + [missedNotifAndUnreadBadgeBgView addSubview:missedNotifAndUnreadBadgeLabel]; + missedNotifAndUnreadBadgeLabel.center = missedNotifAndUnreadBadgeBgView.center; + NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:missedNotifAndUnreadBadgeLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:missedNotifAndUnreadBadgeBgView + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0.0f]; + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:missedNotifAndUnreadBadgeLabel + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:missedNotifAndUnreadBadgeBgView + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0.0f]; + + [NSLayoutConstraint activateConstraints:@[centerXConstraint, centerYConstraint]]; + + } + + return missedNotifAndUnreadBadgeBgView; +} + - (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame { - UIView *sectionHeader = nil; + UIView *sectionHeader = [[UIView alloc] initWithFrame:frame]; + sectionHeader.backgroundColor = kRiotColorLightGrey; + NSInteger sectionBitwise = 0; + UIImageView *chevronView; + UIView *accessoryView; - if (section < sectionsCount && section != searchedRoomIdOrAliasSection) + if (_areSectionsShrinkable) { - NSString* sectionTitle = @""; - NSInteger sectionBitwise = 0; - UIImageView *chevronView; - - if (section == directorySection) + if (section == favoritesSection) { - sectionTitle = NSLocalizedStringFromTable(@"room_recents_directory", @"Vector", nil); + sectionBitwise = RECENTSDATASOURCE_SECTION_FAVORITES; } - else if (section == favoritesSection) + else if (section == peopleSection) { - sectionTitle = NSLocalizedStringFromTable(@"room_recents_favourites", @"Vector", nil); - sectionBitwise = RECENTSDATASOURCE_SECTION_FAVORITES; + sectionBitwise = RECENTSDATASOURCE_SECTION_PEOPLE; } else if (section == conversationSection) { - sectionTitle = NSLocalizedStringFromTable(@"room_recents_conversations", @"Vector", nil); + sectionBitwise = RECENTSDATASOURCE_SECTION_CONVERSATIONS; + } + else if (section == directorySection) + { sectionBitwise = RECENTSDATASOURCE_SECTION_CONVERSATIONS; } else if (section == lowPrioritySection) { - sectionTitle = NSLocalizedStringFromTable(@"room_recents_low_priority", @"Vector", nil); sectionBitwise = RECENTSDATASOURCE_SECTION_LOWPRIORITY; } else if (section == invitesSection) { - sectionTitle = NSLocalizedStringFromTable(@"room_recents_invites", @"Vector", nil); sectionBitwise = RECENTSDATASOURCE_SECTION_INVITES; } - - sectionHeader = [[UIView alloc] initWithFrame:frame]; - sectionHeader.backgroundColor = kRiotColorLightGrey; - - if (sectionBitwise) - { - // Add shrink button - UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom]; - CGRect frame = sectionHeader.frame; - frame.origin.x = frame.origin.y = 0; - shrinkButton.frame = frame; - shrinkButton.backgroundColor = [UIColor clearColor]; - [shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - shrinkButton.tag = sectionBitwise; - [sectionHeader addSubview:shrinkButton]; - sectionHeader.userInteractionEnabled = YES; - - // Add shrink icon - UIImage *chevron; - if (shrinkedSectionsBitMask & sectionBitwise) - { - chevron = [UIImage imageNamed:@"disclosure_icon"]; - } - else - { - chevron = [UIImage imageNamed:@"shrink_icon"]; - } - chevronView = [[UIImageView alloc] initWithImage:chevron]; - chevronView.contentMode = UIViewContentModeCenter; - frame = chevronView.frame; - frame.origin.x = sectionHeader.frame.size.width - frame.size.width - 16; - frame.origin.y = (sectionHeader.frame.size.height - frame.size.height) / 2; - chevronView.frame = frame; - [sectionHeader addSubview:chevronView]; - chevronView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin); - } - - // Add label - frame = sectionHeader.frame; - frame.origin.x = 20; - frame.origin.y = 5; - frame.size.width = chevronView ? chevronView.frame.origin.x - 10 : sectionHeader.frame.size.width - 10; - frame.size.height -= 10; - UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame]; - headerLabel.font = [UIFont boldSystemFontOfSize:15.0]; - headerLabel.backgroundColor = [UIColor clearColor]; - headerLabel.text = sectionTitle; - [sectionHeader addSubview:headerLabel]; } + if (sectionBitwise) + { + // Add shrink button + UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom]; + frame.origin.x = frame.origin.y = 0; + shrinkButton.frame = frame; + shrinkButton.backgroundColor = [UIColor clearColor]; + [shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + shrinkButton.tag = sectionBitwise; + [sectionHeader addSubview:shrinkButton]; + sectionHeader.userInteractionEnabled = YES; + + // Add shrink icon + UIImage *chevron; + if (shrinkedSectionsBitMask & sectionBitwise) + { + chevron = [UIImage imageNamed:@"disclosure_icon"]; + } + else + { + chevron = [UIImage imageNamed:@"shrink_icon"]; + } + chevronView = [[UIImageView alloc] initWithImage:chevron]; + chevronView.contentMode = UIViewContentModeCenter; + frame = chevronView.frame; + frame.origin.x = sectionHeader.frame.size.width - frame.size.width - 16; + frame.origin.y = (sectionHeader.frame.size.height - frame.size.height) / 2; + chevronView.frame = frame; + [sectionHeader addSubview:chevronView]; + chevronView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin); + + accessoryView = chevronView; + } + else if (_recentsDataSourceMode == RecentsDataSourceModeHome) + { + // Add a badge to display the total of missed notifications by section. + accessoryView = [self badgeViewForHeaderTitleInHomeSection:section]; + + if (accessoryView) + { + frame = accessoryView.frame; + frame.origin.x = sectionHeader.frame.size.width - frame.size.width - 16; + frame.origin.y = (sectionHeader.frame.size.height - frame.size.height) / 2; + accessoryView.frame = frame; + [sectionHeader addSubview:accessoryView]; + accessoryView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin); + } + } + + // Add label + frame = sectionHeader.frame; + frame.origin.x = 20; + frame.origin.y = 5; + frame.size.width = accessoryView ? accessoryView.frame.origin.x - 10 : sectionHeader.frame.size.width - 10; + frame.size.height = RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT - 10; + UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame]; + headerLabel.backgroundColor = [UIColor clearColor]; + headerLabel.attributedText = [self attributedStringForHeaderTitleInSection:section]; + [sectionHeader addSubview:headerLabel]; + + if (section == directorySection && _recentsDataSourceMode == RecentsDataSourceModeRooms && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY)) + { + NSLayoutConstraint *leadingConstraint, *trailingConstraint, *topConstraint, *bottomConstraint; + NSLayoutConstraint *widthConstraint, *heightConstraint, *centerYConstraint; + + if (!directorySectionContainer) + { + CGFloat containerWidth = sectionHeader.frame.size.width; + + directorySectionContainer = [[UIView alloc] initWithFrame:CGRectMake(0, RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT, containerWidth, sectionHeader.frame.size.height - RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT)]; + directorySectionContainer.backgroundColor = [UIColor clearColor]; + directorySectionContainer.translatesAutoresizingMaskIntoConstraints = NO; + + // Add the "Network" label at the left + UILabel *networkLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 100, 30)]; + networkLabel.translatesAutoresizingMaskIntoConstraints = NO; + networkLabel.textColor = kRiotTextColorBlack; + networkLabel.font = [UIFont systemFontOfSize:16.0]; + networkLabel.text = NSLocalizedStringFromTable(@"room_recents_directory_section_network", @"Vector", nil); + [directorySectionContainer addSubview:networkLabel]; + + // Add label for selected directory server + directoryServerLabel = [[UILabel alloc] initWithFrame:CGRectMake(120, 0, containerWidth - 32, 30)]; + directoryServerLabel.translatesAutoresizingMaskIntoConstraints = NO; + directoryServerLabel.textColor = kRiotTextColorGray; + directoryServerLabel.font = [UIFont systemFontOfSize:16.0]; + directoryServerLabel.textAlignment = NSTextAlignmentRight; + [directorySectionContainer addSubview:directoryServerLabel]; + + // Chevron + UIImageView *chevronImageView = [[UIImageView alloc] initWithFrame:CGRectMake(containerWidth - 26, 5, 6, 12)]; + chevronImageView.image = [UIImage imageNamed:@"disclosure_icon"]; + chevronImageView.translatesAutoresizingMaskIntoConstraints = NO; + [directorySectionContainer addSubview:chevronImageView]; + + // Set a tap listener on all the container + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onDirectoryServerPickerTap:)]; + [tapGesture setNumberOfTouchesRequired:1]; + [tapGesture setNumberOfTapsRequired:1]; + [directorySectionContainer addGestureRecognizer:tapGesture]; + + // Add networkLabel constraints + centerYConstraint = [NSLayoutConstraint constraintWithItem:networkLabel + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:directorySectionContainer + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0.0f]; + + heightConstraint = [NSLayoutConstraint constraintWithItem:networkLabel + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:30]; + leadingConstraint = [NSLayoutConstraint constraintWithItem:networkLabel + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:directorySectionContainer + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:20]; + widthConstraint = [NSLayoutConstraint constraintWithItem:networkLabel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:100]; + + [NSLayoutConstraint activateConstraints:@[centerYConstraint, heightConstraint, leadingConstraint, widthConstraint]]; + + // Add directoryServerLabel constraints + centerYConstraint = [NSLayoutConstraint constraintWithItem:directoryServerLabel + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:directorySectionContainer + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0.0f]; + + heightConstraint = [NSLayoutConstraint constraintWithItem:directoryServerLabel + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:30]; + leadingConstraint = [NSLayoutConstraint constraintWithItem:directoryServerLabel + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:networkLabel + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:20]; + trailingConstraint = [NSLayoutConstraint constraintWithItem:directoryServerLabel + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:chevronImageView + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:-8]; + + [NSLayoutConstraint activateConstraints:@[centerYConstraint, heightConstraint, leadingConstraint, trailingConstraint]]; + + // Add chevron constraints + trailingConstraint = [NSLayoutConstraint constraintWithItem:chevronImageView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:directorySectionContainer + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:-20]; + + centerYConstraint = [NSLayoutConstraint constraintWithItem:chevronImageView + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:directorySectionContainer + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0.0f]; + + widthConstraint = [NSLayoutConstraint constraintWithItem:chevronImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:6]; + heightConstraint = [NSLayoutConstraint constraintWithItem:chevronImageView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:12]; + + [NSLayoutConstraint activateConstraints:@[trailingConstraint, centerYConstraint, widthConstraint, heightConstraint]]; + } + + // Set the current directory server name + directoryServerLabel.text = _publicRoomsDirectoryDataSource.directoryServerDisplayname; + + // Add the check box container + [sectionHeader addSubview:directorySectionContainer]; + leadingConstraint = [NSLayoutConstraint constraintWithItem:directorySectionContainer + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:0]; + widthConstraint = [NSLayoutConstraint constraintWithItem:directorySectionContainer + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeWidth + multiplier:1 + constant:0]; + topConstraint = [NSLayoutConstraint constraintWithItem:directorySectionContainer + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeTop + multiplier:1 + constant:RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT]; + bottomConstraint = [NSLayoutConstraint constraintWithItem:directorySectionContainer + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:sectionHeader + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:0]; + + [NSLayoutConstraint activateConstraints:@[leadingConstraint, widthConstraint, topConstraint, bottomConstraint]]; + } + return sectionHeader; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == searchedRoomIdOrAliasSection) + // Sanity check + if (tableView.tag != self.recentsDataSourceMode) { - RoomIdOrAliasTableViewCell *roomIdOrAliasCell = [tableView dequeueReusableCellWithIdentifier:RoomIdOrAliasTableViewCell.defaultReuseIdentifier]; - if (!roomIdOrAliasCell) - { - roomIdOrAliasCell = [[RoomIdOrAliasTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[RoomIdOrAliasTableViewCell defaultReuseIdentifier]]; - } - - [roomIdOrAliasCell render:roomIdOrAlias]; - - return roomIdOrAliasCell; + // The view controller of this table view is not the current selected one in the tab bar controller. + // Return a fake cell to prevent app from crashing + return [[UITableViewCell alloc] init]; } - else if (indexPath.section == directorySection) + + if (indexPath.section == directorySection) { - // For the cell showing the public rooms directory search result, - // skip the MatrixKit mechanism and return directly the UITableViewCell - DirectoryRecentTableViewCell *directoryCell = [tableView dequeueReusableCellWithIdentifier:DirectoryRecentTableViewCell.defaultReuseIdentifier]; - if (!directoryCell) - { - directoryCell = [[DirectoryRecentTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[DirectoryRecentTableViewCell defaultReuseIdentifier]]; - } - - [directoryCell render:_publicRoomsDirectoryDataSource]; - - return directoryCell; + NSIndexPath *indexPathInPublicRooms = [NSIndexPath indexPathForRow:indexPath.row inSection:0]; + return [_publicRoomsDirectoryDataSource tableView:tableView cellForRowAtIndexPath:indexPathInPublicRooms]; } - - if (self.droppingCellIndexPath && [indexPath isEqual:self.droppingCellIndexPath]) + else if (self.droppingCellIndexPath && [indexPath isEqual:self.droppingCellIndexPath]) { - static NSString* cellIdentifier = @"VectorRecentsMovingCell"; + static NSString* cellIdentifier = @"RiotRecentsMovingCell"; - UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"VectorRecentsMovingCell"]; + UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"RiotRecentsMovingCell"]; // add an imageview of the cell. // The image is a shot of the genuine cell. @@ -439,6 +808,34 @@ return cell; } + else if ((indexPath.section == conversationSection && !conversationCellDataArray.count) + || (indexPath.section == peopleSection && !peopleCellDataArray.count)) + { + MXKTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCell defaultReuseIdentifier]]; + if (!tableViewCell) + { + tableViewCell = [[MXKTableViewCell alloc] init]; + tableViewCell.textLabel.textColor = kRiotTextColorGray; + tableViewCell.textLabel.font = [UIFont systemFontOfSize:15.0]; + tableViewCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + // Check whether a search session is in progress + if (self.searchPatternsList) + { + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"search_no_result", @"Vector", nil); + } + else if (_recentsDataSourceMode == RecentsDataSourceModePeople || indexPath.section == peopleSection) + { + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"people_no_conversation", @"Vector", nil); + } + else + { + tableViewCell.textLabel.text = NSLocalizedStringFromTable(@"room_recents_no_conversation", @"Vector", nil); + } + + return tableViewCell; + } return [super tableView:tableView cellForRowAtIndexPath:indexPath]; } @@ -466,6 +863,13 @@ cellData = [favoriteCellDataArray objectAtIndex:cellDataIndex]; } } + else if (tableSection == peopleSection) + { + if (cellDataIndex < peopleCellDataArray.count) + { + cellData = [peopleCellDataArray objectAtIndex:cellDataIndex]; + } + } else if (tableSection== conversationSection) { if (cellDataIndex < conversationCellDataArray.count) @@ -493,22 +897,19 @@ - (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == searchedRoomIdOrAliasSection) - { - return RoomIdOrAliasTableViewCell.cellHeight; - } - if (indexPath.section == directorySection) { - // For the cell showing the public rooms directory search result, - // skip the MatrixKit mechanism and return directly the cell height - return DirectoryRecentTableViewCell.cellHeight; + return [_publicRoomsDirectoryDataSource cellHeightAtIndexPath:indexPath]; } - if (self.droppingCellIndexPath && [indexPath isEqual:self.droppingCellIndexPath]) { return self.droppingCellBackGroundView.frame.size.height; } + if ((indexPath.section == conversationSection && !conversationCellDataArray.count) + || (indexPath.section == peopleSection && !peopleCellDataArray.count)) + { + return 50.0; + } // Override this method here to use our own cellDataAtIndexPath id cellData = [self cellDataAtIndexPath:indexPath]; @@ -523,6 +924,21 @@ return 0; } +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Sanity check + if (tableView.tag != self.recentsDataSourceMode) + { + // The view controller of this table view is not the current selected one in the tab bar controller. + return NO; + } + + // Invited rooms are not editable. + return (indexPath.section != invitesSection); +} + +#pragma mark - + - (NSInteger)cellIndexPosWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession within:(NSMutableArray*)cellDataArray { if (roomId && matrixSession && cellDataArray.count) @@ -576,6 +992,21 @@ } } + if (!indexPath && (peopleSection >= 0)) + { + index = [self cellIndexPosWithRoomId:roomId andMatrixSession:matrixSession within:peopleCellDataArray]; + + if (index != NSNotFound) + { + // Check whether the favorites are shrinked + if (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_PEOPLE) + { + return nil; + } + indexPath = [NSIndexPath indexPathForRow:index inSection:peopleSection]; + } + } + if (!indexPath && (conversationSection >= 0)) { index = [self cellIndexPosWithRoomId:roomId andMatrixSession:matrixSession within:conversationCellDataArray]; @@ -616,25 +1047,17 @@ { [invitesCellDataArray removeAllObjects]; [favoriteCellDataArray removeAllObjects]; + [peopleCellDataArray removeAllObjects]; [conversationCellDataArray removeAllObjects]; [lowPriorityCellDataArray removeAllObjects]; - searchedRoomIdOrAliasSection = directorySection = favoritesSection = conversationSection = lowPrioritySection = invitesSection = -1; - sectionsCount = 0; + _missedFavouriteDiscussionsCount = _missedHighlightFavouriteDiscussionsCount = 0; + _missedDirectDiscussionsCount = _missedHighlightDirectDiscussionsCount = 0; + _missedGroupDiscussionsCount = _missedHighlightGroupDiscussionsCount = 0; - if (roomIdOrAlias.length) - { - // The current search pattern corresponds to a valid room id or room alias - searchedRoomIdOrAliasSection = sectionsCount++; - } + directorySection = favoritesSection = peopleSection = conversationSection = lowPrioritySection = invitesSection = -1; - if (!_hidePublicRoomsDirectory) - { - // The public rooms directory cell is then visible whatever the search activity. - directorySection = sectionsCount++; - } - - if (!_hideRecents && displayedRecentsDataSourceArray.count > 0) + if (displayedRecentsDataSourceArray.count > 0) { // FIXME manage multi accounts MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:0]; @@ -642,35 +1065,227 @@ NSInteger count = recentsDataSource.numberOfCells; - for(int index = 0; index < count; index++) + for (int index = 0; index < count; index++) { id recentCellDataStoring = [recentsDataSource cellDataAtIndex:index]; MXRoom* room = recentCellDataStoring.roomSummary.room; - if (room.accountData.tags[kMXRoomTagFavourite]) + if (_recentsDataSourceMode == RecentsDataSourceModeHome) { - [favoriteCellDataArray addObject:recentCellDataStoring]; + if (room.accountData.tags[kMXRoomTagFavourite]) + { + [favoriteCellDataArray addObject:recentCellDataStoring]; + } + else if (room.accountData.tags[kMXRoomTagLowPriority]) + { + [lowPriorityCellDataArray addObject:recentCellDataStoring]; + } + else if (room.state.membership == MXMembershipInvite) + { + [invitesCellDataArray addObject:recentCellDataStoring]; + } + else if (room.isDirect) + { + [peopleCellDataArray addObject:recentCellDataStoring]; + } + else + { + [conversationCellDataArray addObject:recentCellDataStoring]; + } } - else if (room.accountData.tags[kMXRoomTagLowPriority]) + else if (_recentsDataSourceMode == RecentsDataSourceModeFavourites) { - [lowPriorityCellDataArray addObject:recentCellDataStoring]; + // Keep only the favourites rooms. + if (room.accountData.tags[kMXRoomTagFavourite]) + { + [favoriteCellDataArray addObject:recentCellDataStoring]; + } + } + else if (_recentsDataSourceMode == RecentsDataSourceModePeople) + { + // Keep only the direct rooms which are not low priority + if (room.isDirect && !room.accountData.tags[kMXRoomTagLowPriority]) + { + if (room.state.membership == MXMembershipInvite) + { + [invitesCellDataArray addObject:recentCellDataStoring]; + } + else + { + [conversationCellDataArray addObject:recentCellDataStoring]; + } + } + } + else if (_recentsDataSourceMode == RecentsDataSourceModeRooms) + { + // Consider only non direct rooms. + if (!room.isDirect) + { + // Keep only the invites, the favourites and the rooms without tag + if (room.state.membership == MXMembershipInvite) + { + [invitesCellDataArray addObject:recentCellDataStoring]; + } + else if (!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) + { + [conversationCellDataArray addObject:recentCellDataStoring]; + } + } + } + + // Update missed conversations counts + NSUInteger notificationCount = recentCellDataStoring.roomSummary.notificationCount; + + // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. + if (room.isMentionsOnly) + { + // Only the highlighted missed messages must be considered here. + notificationCount = recentCellDataStoring.roomSummary.highlightCount; + } + + if (notificationCount) + { + if (room.accountData.tags[kMXRoomTagFavourite]) + { + _missedFavouriteDiscussionsCount ++; + + if (recentCellDataStoring.roomSummary.highlightCount) + { + _missedHighlightFavouriteDiscussionsCount ++; + } + } + + if (room.isDirect) + { + _missedDirectDiscussionsCount ++; + + if (recentCellDataStoring.roomSummary.highlightCount) + { + _missedHighlightDirectDiscussionsCount ++; + } + } + else if (!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) + { + _missedGroupDiscussionsCount ++; + + if (recentCellDataStoring.roomSummary.highlightCount) + { + _missedHighlightGroupDiscussionsCount ++; + } + } } else if (room.state.membership == MXMembershipInvite) { - [invitesCellDataArray addObject:recentCellDataStoring]; + if (room.isDirect) + { + _missedDirectDiscussionsCount ++; + } + else + { + _missedGroupDiscussionsCount ++; + } } - else - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - - if (invitesCellDataArray.count > 0) - { - invitesSection = sectionsCount++; + } - if (favoriteCellDataArray.count > 0) + if (_recentsDataSourceMode == RecentsDataSourceModeHome) + { + BOOL pinMissedNotif = [[NSUserDefaults standardUserDefaults] boolForKey:@"pinRoomsWithMissedNotif"]; + BOOL pinUnread = [[NSUserDefaults standardUserDefaults] boolForKey:@"pinRoomsWithUnread"]; + NSComparator comparator = nil; + + if (pinMissedNotif) + { + // Sort each rooms collection by considering first the rooms with some missed notifs, the rooms with unread, then the others. + comparator = ^NSComparisonResult(id recentCellData1, id recentCellData2) { + + if (recentCellData1.highlightCount) + { + if (recentCellData2.highlightCount) + { + return NSOrderedSame; + } + else + { + return NSOrderedAscending; + } + } + else if (recentCellData2.highlightCount) + { + return NSOrderedDescending; + } + else if (recentCellData1.notificationCount) + { + if (recentCellData2.notificationCount) + { + return NSOrderedSame; + } + else + { + return NSOrderedAscending; + } + } + else if (recentCellData2.notificationCount) + { + return NSOrderedDescending; + } + else if (pinUnread) + { + if (recentCellData1.hasUnread) + { + if (recentCellData2.hasUnread) + { + return NSOrderedSame; + } + else + { + return NSOrderedAscending; + } + } + else if (recentCellData2.hasUnread) + { + return NSOrderedDescending; + } + } + + return NSOrderedSame; + }; + } + else if (pinUnread) + { + // Sort each rooms collection by considering first the rooms with some unread messages then the others. + comparator = ^NSComparisonResult(id recentCellData1, id recentCellData2) { + + if (recentCellData1.hasUnread) + { + if (recentCellData2.hasUnread) + { + return NSOrderedSame; + } + else + { + return NSOrderedAscending; + } + } + else if (recentCellData2.hasUnread) + { + return NSOrderedDescending; + } + + return NSOrderedSame; + }; + } + + if (comparator) + { + // Sort the rooms collections + [favoriteCellDataArray sortUsingComparator:comparator]; + [peopleCellDataArray sortUsingComparator:comparator]; + [conversationCellDataArray sortUsingComparator:comparator]; + [lowPriorityCellDataArray sortUsingComparator:comparator]; + } + } + else if (favoriteCellDataArray.count > 0 && _recentsDataSourceMode == RecentsDataSourceModeFavourites) { // Sort them according to their tag order [favoriteCellDataArray sortUsingComparator:^NSComparisonResult(id recentCellData1, id recentCellData2) { @@ -678,23 +1293,6 @@ return [session compareRoomsByTag:kMXRoomTagFavourite room1:recentCellData1.roomSummary.room room2:recentCellData2.roomSummary.room]; }]; - favoritesSection = sectionsCount++; - } - - if (conversationCellDataArray.count > 0) - { - conversationSection = sectionsCount++; - } - - if (lowPriorityCellDataArray.count > 0) - { - // Sort them according to their tag order - [lowPriorityCellDataArray sortUsingComparator:^NSComparisonResult(id recentCellData1, id recentCellData2) { - - return [session compareRoomsByTag:kMXRoomTagLowPriority room1:recentCellData1.roomSummary.room room2:recentCellData2.roomSummary.room]; - - }]; - lowPrioritySection = sectionsCount++; } } } @@ -725,6 +1323,18 @@ [super dataSource:dataSource didCellChange:changes]; } +#pragma mark - Drag & Drop handling + +- (BOOL)isMovingCellSection:(NSInteger)section +{ + return self.droppingCellIndexPath && (self.droppingCellIndexPath.section == section); +} + +- (BOOL)isHiddenCellSection:(NSInteger)section +{ + return self.hiddenCellIndexPath && (self.hiddenCellIndexPath.section == section); +} + #pragma mark - Action - (IBAction)onButtonPressed:(id)sender @@ -760,9 +1370,17 @@ publicRoomsTriggerTimer = nil; _publicRoomsDirectoryDataSource.searchPattern = searchPattern; + [_publicRoomsDirectoryDataSource paginate:nil failure:nil]; } } +#pragma mark - Action + +- (IBAction)onDirectoryServerPickerTap:(UITapGestureRecognizer*)sender +{ + [self.delegate dataSource:self didRecognizeAction:kRecentsDataSourceTapOnDirectoryServerChange inCell:nil userInfo:nil]; +} + #pragma mark - Override MXKDataSource - (void)destroy @@ -774,25 +1392,9 @@ } #pragma mark - Override MXKRecentsDataSource + - (void)searchWithPatterns:(NSArray *)patternsList { - // Check whether the typed input is a room alias or a room identifier. - roomIdOrAlias = nil; - if (patternsList.count == 1) - { - NSString *pattern = patternsList[0]; - - if ([MXTools isMatrixRoomAlias:pattern] || [MXTools isMatrixRoomIdentifier:pattern]) - { - // Display this room id/alias only if it is not already joined by the user - MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; - if (![accountManager accountKnowingRoomWithRoomIdOrAlias:pattern]) - { - roomIdOrAlias = pattern; - } - } - } - [super searchWithPatterns:patternsList]; if (_publicRoomsDirectoryDataSource) @@ -806,17 +1408,16 @@ } } -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - // Invited rooms are not editable. - return (indexPath.section != invitesSection); -} - #pragma mark - drag and drop managemenent - (BOOL)isDraggableCellAt:(NSIndexPath*)path { - return (path && ((path.section == favoritesSection) || (path.section == lowPrioritySection) || (path.section == conversationSection))); + if (_recentsDataSourceMode == RecentsDataSourceModePeople || _recentsDataSourceMode == RecentsDataSourceModeRooms) + { + return NO; + } + + return (path && ((path.section == favoritesSection) || (path.section == peopleSection) || (path.section == lowPrioritySection) || (path.section == conversationSection))); } - (BOOL)canCellMoveFrom:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath @@ -855,42 +1456,65 @@ if ([self canCellMoveFrom:oldPath to:newPath] && ![newPath isEqual:oldPath]) { - NSString* oldRoomTag = [self roomTagAt:oldPath]; - NSString* dstRoomTag = [self roomTagAt:newPath]; - NSUInteger oldPos = (oldPath.section == newPath.section) ? oldPath.row : NSNotFound; - - NSString* tagOrder = [room.mxSession tagOrderToBeAtIndex:newPath.row from:oldPos withTag:dstRoomTag]; - - NSLog(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.state.roomId, room.riotDisplayname, oldRoomTag, dstRoomTag, tagOrder); - - [room replaceTag:oldRoomTag - byTag:dstRoomTag - withOrder:tagOrder - success: ^{ - - NSLog(@"[RecentsDataSource] move is done"); - - if (moveSuccess) - { - moveSuccess(); - } - - // wait the server echo to reload the tableview. - - } failure:^(NSError *error) { - - NSLog(@"[RecentsDataSource] Failed to update the tag %@ of room (%@)", dstRoomTag, room.state.roomId); - - if (moveFailure) - { - moveFailure(error); - } - - [self refreshRoomsSectionsAndReload]; - - // Notify MatrixKit user - [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error]; - }]; + if (newPath.section == peopleSection) + { + [room setIsDirect:YES + withUserId:nil + success:moveSuccess + failure:^(NSError *error) { + + NSLog(@"[RecentsDataSource] Failed to mark as direct"); + + if (moveFailure) + { + moveFailure(error); + } + + [self forceRefresh]; + + // Notify user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + else + { + NSString* oldRoomTag = [self roomTagAt:oldPath]; + NSString* dstRoomTag = [self roomTagAt:newPath]; + NSUInteger oldPos = (oldPath.section == newPath.section) ? oldPath.row : NSNotFound; + + NSString* tagOrder = [room.mxSession tagOrderToBeAtIndex:newPath.row from:oldPos withTag:dstRoomTag]; + + NSLog(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.state.roomId, room.riotDisplayname, oldRoomTag, dstRoomTag, tagOrder); + + [room replaceTag:oldRoomTag + byTag:dstRoomTag + withOrder:tagOrder + success: ^{ + + NSLog(@"[RecentsDataSource] move is done"); + + if (moveSuccess) + { + moveSuccess(); + } + + // wait the server echo to reload the tableview. + + } failure:^(NSError *error) { + + NSLog(@"[RecentsDataSource] Failed to update the tag %@ of room (%@)", dstRoomTag, room.state.roomId); + + if (moveFailure) + { + moveFailure(error); + } + + [self forceRefresh]; + + // Notify user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } } else { @@ -901,7 +1525,7 @@ moveFailure(nil); } - [self refreshRoomsSectionsAndReload]; + [self forceRefresh]; } } diff --git a/Riot/Model/RoomList/UnifiedSearchRecentsDataSource.h b/Riot/Model/RoomList/UnifiedSearchRecentsDataSource.h new file mode 100644 index 000000000..6102584de --- /dev/null +++ b/Riot/Model/RoomList/UnifiedSearchRecentsDataSource.h @@ -0,0 +1,34 @@ +/* + Copyright 2017 Vector Creations 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 "RecentsDataSource.h" + +/** + 'UnifiedSearchRecentsDataSource' class inherits from 'RecentsDataSource' to define the Riot recents source + used during the unified search on rooms. + */ +@interface UnifiedSearchRecentsDataSource : RecentsDataSource + +#pragma mark - Directory handling + +/** + Hide recents. NO by default. + */ +@property (nonatomic) BOOL hideRecents; + +@end diff --git a/Riot/Model/RoomList/UnifiedSearchRecentsDataSource.m b/Riot/Model/RoomList/UnifiedSearchRecentsDataSource.m new file mode 100644 index 000000000..547972da0 --- /dev/null +++ b/Riot/Model/RoomList/UnifiedSearchRecentsDataSource.m @@ -0,0 +1,228 @@ +/* + Copyright 2017 Vector Creations 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 "UnifiedSearchRecentsDataSource.h" + +#import "RiotDesignValues.h" + +#import "RoomIdOrAliasTableViewCell.h" +#import "DirectoryRecentTableViewCell.h" + +#import "MXRoom+Riot.h" + +@interface UnifiedSearchRecentsDataSource() +{ + NSInteger searchedRoomIdOrAliasSection; // used to display the potential room id or alias typed during search. + + // The potential room id or alias typed in search input. + NSString *roomIdOrAlias; +} +@end + +@implementation UnifiedSearchRecentsDataSource + +- (instancetype)init +{ + self = [super init]; + if (self) + { + searchedRoomIdOrAliasSection = -1; + + _hideRecents = NO; + } + return self; +} + +#pragma mark - + +- (void)setPublicRoomsDirectoryDataSource:(PublicRoomsDirectoryDataSource *)publicRoomsDirectoryDataSource +{ + [super setPublicRoomsDirectoryDataSource:publicRoomsDirectoryDataSource]; + + // Start by looking for all public rooms + [self.publicRoomsDirectoryDataSource paginate:nil failure:nil]; +} + +- (void)setHideRecents:(BOOL)hideRecents +{ + if (_hideRecents != hideRecents) + { + _hideRecents = hideRecents; + + [self forceRefresh]; + } +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + NSInteger sectionsCount = 0; + + // Check whether all data sources are ready before rendering recents + if (self.state == MXKDataSourceStateReady) + { + sectionsCount = [super numberOfSectionsInTableView:tableView]; + NSInteger sectionsOffset = 0; + + if (roomIdOrAlias.length) + { + // The current search pattern corresponds to a valid room id or room alias + searchedRoomIdOrAliasSection = sectionsOffset++; + } + + // The public rooms directory cell is then visible whatever the search activity. + self.directorySection = sectionsOffset++; + + if (_hideRecents) + { + self.invitesSection = self.favoritesSection = self.peopleSection = self.conversationSection = self.lowPrioritySection = -1; + sectionsCount = sectionsOffset; + } + else + { + if (self.invitesSection != -1) + { + self.invitesSection += sectionsOffset; + } + if (self.favoritesSection != -1) + { + self.favoritesSection += sectionsOffset; + } + if (self.peopleSection != -1) + { + self.peopleSection += sectionsOffset; + } + if (self.conversationSection != -1) + { + self.conversationSection += sectionsOffset; + } + if (self.lowPrioritySection != -1) + { + self.lowPrioritySection += sectionsOffset; + } + sectionsCount += sectionsOffset; + } + } + return sectionsCount; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + NSUInteger count = 0; + + if (section == searchedRoomIdOrAliasSection) + { + count = 1; + } + else if (section == self.directorySection) + { + count = 1; + } + else + { + count = [super tableView:tableView numberOfRowsInSection:section]; + } + + return count; +} + +- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame +{ + UIView *sectionHeader = nil; + + if (section != searchedRoomIdOrAliasSection) + { + sectionHeader = [super viewForHeaderInSection:section withFrame:frame]; + } + + return sectionHeader; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == searchedRoomIdOrAliasSection) + { + RoomIdOrAliasTableViewCell *roomIdOrAliasCell = [tableView dequeueReusableCellWithIdentifier:RoomIdOrAliasTableViewCell.defaultReuseIdentifier]; + if (!roomIdOrAliasCell) + { + roomIdOrAliasCell = [[RoomIdOrAliasTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[RoomIdOrAliasTableViewCell defaultReuseIdentifier]]; + } + + [roomIdOrAliasCell render:roomIdOrAlias]; + + return roomIdOrAliasCell; + } + else if (indexPath.section == self.directorySection) + { + // For the cell showing the public rooms directory search result, + // skip the MatrixKit mechanism and return directly the UITableViewCell + DirectoryRecentTableViewCell *directoryCell = [tableView dequeueReusableCellWithIdentifier:DirectoryRecentTableViewCell.defaultReuseIdentifier]; + if (!directoryCell) + { + directoryCell = [[DirectoryRecentTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[DirectoryRecentTableViewCell defaultReuseIdentifier]]; + } + + [directoryCell render:self.publicRoomsDirectoryDataSource]; + + return directoryCell; + } + + return [super tableView:tableView cellForRowAtIndexPath:indexPath]; +} + +- (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == searchedRoomIdOrAliasSection) + { + return RoomIdOrAliasTableViewCell.cellHeight; + } + + if (indexPath.section == self.directorySection) + { + // For the cell showing the public rooms directory search result, + // skip the MatrixKit mechanism and return directly the cell height + return DirectoryRecentTableViewCell.cellHeight; + } + + return [super cellHeightAtIndexPath:indexPath]; +} + +#pragma mark - Override MXKRecentsDataSource + +- (void)searchWithPatterns:(NSArray *)patternsList +{ + // Check whether the typed input is a room alias or a room identifier. + roomIdOrAlias = nil; + if (patternsList.count == 1) + { + NSString *pattern = patternsList[0]; + + if ([MXTools isMatrixRoomAlias:pattern] || [MXTools isMatrixRoomIdentifier:pattern]) + { + // Display this room id/alias only if it is not already joined by the user + MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; + if (![accountManager accountKnowingRoomWithRoomIdOrAlias:pattern]) + { + roomIdOrAlias = pattern; + } + } + } + + [super searchWithPatterns:patternsList]; +} + +@end diff --git a/Riot/Model/Search/FilesSearchCellData.m b/Riot/Model/Search/FilesSearchCellData.m index 03ba0b808..f8803ff79 100644 --- a/Riot/Model/Search/FilesSearchCellData.m +++ b/Riot/Model/Search/FilesSearchCellData.m @@ -38,6 +38,13 @@ // Title is here the file name stored in event body title = [event.content[@"body"] isKindOfClass:[NSString class]] ? event.content[@"body"] : nil; + // Check attachment if any + if ([searchDataSource.eventFormatter isSupportedAttachment:event]) + { + // Note: event.eventType is equal here to MXEventTypeRoomMessage + attachment = [[MXKAttachment alloc] initWithEvent:event andMatrixSession:searchDataSource.mxSession]; + } + // Append the file size if any if (attachment.contentInfo[@"size"]) { diff --git a/Riot/Riot-Defaults.plist b/Riot/Riot-Defaults.plist index 691c83aea..954859b75 100644 --- a/Riot/Riot-Defaults.plist +++ b/Riot/Riot-Defaults.plist @@ -2,6 +2,10 @@ + pinRoomsWithMissedNotif + + pinRoomsWithUnread + pushGatewayURL https://matrix.org/_matrix/push/v1/notify pusherAppIdDev @@ -42,5 +46,14 @@ 15066368 presenceColorForOfflineUser 15020851 + bugReportEndpointUrl + https://riot.im/bugreports + bugReportApp + riot-ios + roomDirectoryServers + + matrix.org + + diff --git a/Riot/Utils/AvatarGenerator.m b/Riot/Utils/AvatarGenerator.m index fa840f055..c1e0a28af 100644 --- a/Riot/Utils/AvatarGenerator.m +++ b/Riot/Utils/AvatarGenerator.m @@ -36,7 +36,7 @@ static UILabel* backgroundLabel = nil; colorsList = [[NSMutableArray alloc] init]; [colorsList addObject:kRiotColorGreen]; [colorsList addObject:kRiotColorLightGreen]; - [colorsList addObject:kRiotColorOrange]; + [colorsList addObject:kRiotColorLightOrange]; } } diff --git a/Riot/Utils/RiotDesignValues.h b/Riot/Utils/RiotDesignValues.h index 5cb5817ff..268fe8946 100644 --- a/Riot/Utils/RiotDesignValues.h +++ b/Riot/Utils/RiotDesignValues.h @@ -30,10 +30,12 @@ extern UIColor *kRiotColorGreen; extern UIColor *kRiotColorLightGreen; extern UIColor *kRiotColorLightGrey; +extern UIColor *kRiotColorLightOrange; extern UIColor *kRiotColorSilver; -extern UIColor *kRiotColorOrange; extern UIColor *kRiotColorPinkRed; extern UIColor *kRiotColorRed; +extern UIColor *kRiotColorIndigo; +extern UIColor *kRiotColorOrange; #pragma mark - Riot Text Colors extern UIColor *kRiotTextColorBlack; diff --git a/Riot/Utils/RiotDesignValues.m b/Riot/Utils/RiotDesignValues.m index ce31ac422..bceb38954 100644 --- a/Riot/Utils/RiotDesignValues.m +++ b/Riot/Utils/RiotDesignValues.m @@ -20,10 +20,12 @@ UIColor *kRiotColorGreen; UIColor *kRiotColorLightGreen; UIColor *kRiotColorLightGrey; +UIColor *kRiotColorLightOrange; UIColor *kRiotColorSilver; -UIColor *kRiotColorOrange; UIColor *kRiotColorPinkRed; UIColor *kRiotColorRed; +UIColor *kRiotColorIndigo; +UIColor *kRiotColorOrange; UIColor *kRiotTextColorBlack; UIColor *kRiotTextColorDarkGray; @@ -43,11 +45,13 @@ NSInteger const kRiotRoomAdminLevel = 100; // Load colors at the app load time for the life of the app // Colors as defined by the design - kRiotColorGreen = [UIColor colorWithRed:(98.0/255.0) green:(206.0/255.0) blue:(156.0/255.0) alpha:1.0]; + kRiotColorGreen = UIColorFromRGB(0x62CE9C); kRiotColorLightGrey = [UIColor colorWithRed:(242.0 / 255.0) green:(242.0 / 255.0) blue:(242.0 / 255.0) alpha:1.0]; - kRiotColorSilver = [UIColor colorWithRed:(199.0 / 255.0) green:(199.0 / 255.0) blue:(204.0 / 255.0) alpha:1.0]; - kRiotColorPinkRed = [UIColor colorWithRed:(255.0 / 255.0) green:(0.0 / 255.0) blue:(100.0 / 255.0) alpha:1.0]; + kRiotColorSilver = UIColorFromRGB(0xC7C7CC); + kRiotColorPinkRed = UIColorFromRGB(0xFF0064); kRiotColorRed = UIColorFromRGB(0xFF4444); + kRiotColorIndigo = UIColorFromRGB(0xBD79CC); + kRiotColorOrange = UIColorFromRGB(0xF8A15F); kRiotTextColorBlack = [UIColor colorWithRed:(60.0 / 255.0) green:(60.0 / 255.0) blue:(60.0 / 255.0) alpha:1.0]; kRiotTextColorDarkGray = [UIColor colorWithRed:(74.0 / 255.0) green:(74.0 / 255.0) blue:(74.0 / 255.0) alpha:1.0]; @@ -57,7 +61,7 @@ NSInteger const kRiotRoomAdminLevel = 100; // Colors copied from Vector web kRiotColorLightGreen = UIColorFromRGB(0x50e2c2); - kRiotColorOrange = UIColorFromRGB(0xf4c371); + kRiotColorLightOrange = UIColorFromRGB(0xf4c371); } @end diff --git a/Riot/ViewController/AttachmentsViewController.m b/Riot/ViewController/AttachmentsViewController.m index 00c676fa2..589b97bfc 100644 --- a/Riot/ViewController/AttachmentsViewController.m +++ b/Riot/ViewController/AttachmentsViewController.m @@ -19,10 +19,6 @@ #import "AppDelegate.h" -#import "RageShakeManager.h" - -#import "RiotDesignValues.h" - @implementation AttachmentsViewController #pragma mark - diff --git a/Riot/ViewController/AuthenticationViewController.m b/Riot/ViewController/AuthenticationViewController.m index a14f0b953..ba7a9819f 100644 --- a/Riot/ViewController/AuthenticationViewController.m +++ b/Riot/ViewController/AuthenticationViewController.m @@ -22,10 +22,6 @@ #import "AuthInputsView.h" #import "ForgotPasswordInputsView.h" -#import "RageShakeManager.h" - -#import "RiotDesignValues.h" - @interface AuthenticationViewController () { /** diff --git a/Riot/ViewController/BugReportViewController.h b/Riot/ViewController/BugReportViewController.h new file mode 100644 index 000000000..1182622bc --- /dev/null +++ b/Riot/ViewController/BugReportViewController.h @@ -0,0 +1,66 @@ +/* + Copyright 2017 Vector Creations 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 BugReportViewController : MXKViewController + +@property (weak, nonatomic) IBOutlet UIScrollView *scrollView; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scrollViewBottomConstraint; + +@property (weak, nonatomic) IBOutlet UIView *containerView; +@property (weak, nonatomic) IBOutlet UILabel *titleLabel; + +@property (weak, nonatomic) IBOutlet UIView *bugDescriptionContainer; + +@property (weak, nonatomic) IBOutlet UILabel *descriptionLabel; +@property (weak, nonatomic) IBOutlet UITextView *bugReportDescriptionTextView; +@property (weak, nonatomic) IBOutlet UILabel *logsDescriptionLabel; + +@property (weak, nonatomic) IBOutlet UIView *sendLogsContainer; +@property (weak, nonatomic) IBOutlet UILabel *sendLogsLabel; +@property (weak, nonatomic) IBOutlet UIImageView *sendLogsButtonImage; + +@property (weak, nonatomic) IBOutlet UIView *sendScreenshotContainer; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *sendScreenshotContainerHeightConstraint; +@property (weak, nonatomic) IBOutlet UILabel *sendScreenshotLabel; +@property (weak, nonatomic) IBOutlet UIImageView *sendScreenshotButtonImage; + +@property (weak, nonatomic) IBOutlet UIView *sendingContainer; +@property (weak, nonatomic) IBOutlet UILabel *sendingLabel; +@property (weak, nonatomic) IBOutlet UIProgressView *sendingProgress; + +@property (weak, nonatomic) IBOutlet UIButton *cancelButton; +@property (weak, nonatomic) IBOutlet UIButton *sendButton; + ++ (instancetype)bugReportViewController; + +- (void)showInViewController:(UIViewController*)viewController; + +/** + The screenshot to send with the bug report. + */ +@property (nonatomic) UIImage *screenshot; + +/** + Option to report a crash. + The crash log will sent in the report. + */ +@property (nonatomic) BOOL reportCrash; + +@end diff --git a/Riot/ViewController/BugReportViewController.m b/Riot/ViewController/BugReportViewController.m new file mode 100644 index 000000000..e1eacfe20 --- /dev/null +++ b/Riot/ViewController/BugReportViewController.m @@ -0,0 +1,350 @@ +/* + Copyright 2017 Vector Creations 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 "BugReportViewController.h" + +#import "AppDelegate.h" + +#import "GBDeviceInfo_iOS.h" + +@interface BugReportViewController () +{ + MXBugReportRestClient *bugReportRestClient; + + // The temporary file used to store the screenshot + NSURL *screenShotFile; +} + +@property (nonatomic) BOOL sendLogs; +@property (nonatomic) BOOL sendScreenshot; + +@end + +@implementation BugReportViewController + +#pragma mark - Class methods + ++ (UINib *)nib +{ + return [UINib nibWithNibName:NSStringFromClass([BugReportViewController class]) + bundle:[NSBundle bundleForClass:[BugReportViewController class]]]; +} + ++ (instancetype)bugReportViewController +{ + return [[[self class] alloc] initWithNibName:NSStringFromClass([BugReportViewController class]) + bundle:[NSBundle bundleForClass:[BugReportViewController class]]]; +} + +#pragma mark - + +- (void)showInViewController:(UIViewController *)viewController +{ + self.providesPresentationContextTransitionStyle = YES; + self.definesPresentationContext = YES; + self.modalPresentationStyle = UIModalPresentationOverFullScreen; + self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + + [viewController presentViewController:self animated:YES completion:nil]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _logsDescriptionLabel.text = NSLocalizedStringFromTable(@"bug_report_logs_description", @"Vector", nil); + _sendLogsLabel.text = NSLocalizedStringFromTable(@"bug_report_send_logs", @"Vector", nil); + _sendScreenshotLabel.text = NSLocalizedStringFromTable(@"bug_report_send_screenshot", @"Vector", nil); + + _containerView.layer.cornerRadius = 20; + + _bugReportDescriptionTextView.layer.borderWidth = 1.0f; + _bugReportDescriptionTextView.layer.borderColor = kRiotColorLightGrey.CGColor; + _bugReportDescriptionTextView.text = nil; + _bugReportDescriptionTextView.delegate = self; + + if (_reportCrash) + { + _titleLabel.text = NSLocalizedStringFromTable(@"bug_crash_report_title", @"Vector", nil); + _descriptionLabel.text = NSLocalizedStringFromTable(@"bug_crash_report_description", @"Vector", nil); + } + else + { + _titleLabel.text = NSLocalizedStringFromTable(@"bug_report_title", @"Vector", nil); + _descriptionLabel.text = NSLocalizedStringFromTable(@"bug_report_description", @"Vector", nil); + + // Allow to send empty description for crash report but not for bug report + _sendButton.enabled = NO; + } + + _sendingContainer.hidden = YES; + + self.sendLogs = YES; + self.sendScreenshot = YES; + + // Hide the screenshot button if there is no screenshot + if (!_screenshot) + { + _sendScreenshotContainer.hidden = YES; + _sendScreenshotContainerHeightConstraint.constant = 0; + } + + // Listen to sendLogs tap + UITapGestureRecognizer *sendLogsTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSendLogsTap:)]; + [sendLogsTapGesture setNumberOfTouchesRequired:1]; + [_sendLogsContainer addGestureRecognizer:sendLogsTapGesture]; + _sendLogsContainer.userInteractionEnabled = YES; + + // Listen to sendScreenshot tap + UITapGestureRecognizer *sendScreenshotTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onSendScreenshotTap:)]; + [sendScreenshotTapGesture setNumberOfTouchesRequired:1]; + [_sendScreenshotContainer addGestureRecognizer:sendScreenshotTapGesture]; + _sendScreenshotContainer.userInteractionEnabled = YES; + + // Add an accessory view in order to retrieve keyboard view + _bugReportDescriptionTextView.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero]; +} + +- (void)dealloc +{ + _bugReportDescriptionTextView.inputAccessoryView = nil; +} + +-(void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + if (screenShotFile) + { + [[NSFileManager defaultManager] removeItemAtURL:screenShotFile error:nil]; + screenShotFile = nil; + } +} + +- (void)setSendLogs:(BOOL)sendLogs +{ + _sendLogs = sendLogs; + if (_sendLogs) + { + _sendLogsButtonImage.image = [UIImage imageNamed:@"selection_tick"]; + } + else + { + _sendLogsButtonImage.image = [UIImage imageNamed:@"selection_untick"]; + } +} + +- (void)setSendScreenshot:(BOOL)sendScreenshot +{ + _sendScreenshot = sendScreenshot; + if (_sendScreenshot) + { + _sendScreenshotButtonImage.image = [UIImage imageNamed:@"selection_tick"]; + } + else + { + _sendScreenshotButtonImage.image = [UIImage imageNamed:@"selection_untick"]; + } +} + +#pragma mark - MXKViewController +- (void)onKeyboardShowAnimationComplete +{ + self.keyboardView = _bugReportDescriptionTextView.inputAccessoryView.superview; +} + +-(void)setKeyboardHeight:(CGFloat)keyboardHeight +{ + // In portrait in 6/7 and 6+/7+, make the height of the popup smaller to be able to + // display Cancel and Send buttons. + // Do nothing in landscape or in 5 in portrait and in landscape. There will be not enough + // room to display bugReportDescriptionTextView. + if (self.view.frame.size.height > 568) + { + self.scrollViewBottomConstraint.constant = keyboardHeight; + } + else + { + self.scrollViewBottomConstraint.constant = 0; + } + + [self.view layoutIfNeeded]; +} + +#pragma mark - UITextViewDelegate + +- (void)textViewDidChange:(UITextView *)textView +{ + _sendButton.enabled = (_bugReportDescriptionTextView.text.length != 0); +} + +#pragma mark - User actions + +- (IBAction)onSendButtonPress:(id)sender +{ + _sendButton.hidden = YES; + _sendingContainer.hidden = NO; + + // Setup data to send + NSString *url = [[NSUserDefaults standardUserDefaults] objectForKey:@"bugReportEndpointUrl"]; + bugReportRestClient = [[MXBugReportRestClient alloc] initWithBugReportEndpoint:url]; + + // App info + bugReportRestClient.appName = [[NSUserDefaults standardUserDefaults] objectForKey:@"bugReportApp"]; // Use the name allocated by the bug report server + bugReportRestClient.version = [AppDelegate theDelegate].appVersion; + bugReportRestClient.build = [AppDelegate theDelegate].build; + + // Device info + bugReportRestClient.deviceModel = [GBDeviceInfo deviceInfo].modelString; + bugReportRestClient.deviceOS = [NSString stringWithFormat:@"%@ %@", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion]]; + + // User info (TODO: handle multi-account and find a way to expose them in rageshake API) + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + MXKAccount *mainAccount = [MXKAccountManager sharedManager].accounts.firstObject; + if (mainAccount.mxSession.myUser.userId) + { + userInfo[@"user_id"] = mainAccount.mxSession.myUser.userId; + } + if (mainAccount.mxSession.matrixRestClient.credentials.deviceId) + { + userInfo[@"device_id"] = mainAccount.mxSession.matrixRestClient.credentials.deviceId; + } + + userInfo[@"locale"] = [NSLocale preferredLanguages][0]; + userInfo[@"app_language"] = [[NSBundle mainBundle] preferredLocalizations][0]; + + bugReportRestClient.others = userInfo; + + // Screenshot + NSArray *files; + if (_screenshot && _sendScreenshot) + { + // Store the screenshot into a temporary file + NSData *screenShotData = UIImagePNGRepresentation(_screenshot); + screenShotFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"screenshot.png"]]; + [screenShotData writeToURL:screenShotFile atomically:YES]; + + files = @[screenShotFile]; + } + + // Prepare labels to attach to the GitHub issue + NSMutableArray *gitHubLabels = [NSMutableArray array]; + if (_reportCrash) + { + // Label the GH issue as "crash" + [gitHubLabels addObject:@"crash"]; + } + + // Add a Github label giving information about the version + if (bugReportRestClient.version && bugReportRestClient.build) + { + NSString *build = bugReportRestClient.build; + NSString *versionLabel = bugReportRestClient.version; + + // If this is not the app store version, be more accurate on the build origin + if ([build isEqualToString:NSLocalizedStringFromTable(@"settings_config_no_build_info", @"Vector", nil)]) + { + // This is a debug session from Xcode + versionLabel = [versionLabel stringByAppendingString:@"-debug"]; + } + else if (build && ![build containsString:@"master"]) + { + // This is a Jenkins build. Add the branch and the build number + NSString *buildString = [build stringByReplacingOccurrencesOfString:@" " withString:@"-"]; + versionLabel = [[versionLabel stringByAppendingString:@"-"] stringByAppendingString:buildString]; + } + + [gitHubLabels addObject:versionLabel]; + } + + // Submit + [bugReportRestClient sendBugReport:_bugReportDescriptionTextView.text sendLogs:_sendLogs sendCrashLog:_reportCrash sendFiles:files attachGitHubLabels:gitHubLabels progress:^(MXBugReportState state, NSProgress *progress) { + + switch (state) + { + case MXBugReportStateProgressZipping: + _sendingLabel.text = NSLocalizedStringFromTable(@"bug_report_progress_zipping", @"Vector", nil); + break; + + case MXBugReportStateProgressUploading: + _sendingLabel.text = NSLocalizedStringFromTable(@"bug_report_progress_uploading", @"Vector", nil); + break; + + default: + break; + } + + _sendingProgress.progress = progress.fractionCompleted; + + } success:^{ + + bugReportRestClient = nil; + + if (_reportCrash) + { + // Erase the crash log + [MXLogger deleteCrashLog]; + } + + [self dismissViewControllerAnimated:YES completion:nil]; + + } failure:^(NSError *error) { + + bugReportRestClient = nil; + + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + _sendButton.hidden = NO; + _sendingContainer.hidden = YES; + }]; +} + +- (IBAction)onCancelButtonPressed:(id)sender +{ + if (bugReportRestClient) + { + // If the submission is in progress, cancel the sending and come back + // to the bug report screen + [bugReportRestClient cancel]; + bugReportRestClient = nil; + + _sendButton.hidden = NO; + _sendingContainer.hidden = YES; + } + else + { + if (_reportCrash) + { + // Erase the crash log + [MXLogger deleteCrashLog]; + } + + // Else, lease the bug report screen + [self dismissViewControllerAnimated:YES completion:nil]; + } +} + +- (IBAction)onSendLogsTap:(id)sender +{ + self.sendLogs = !self.sendLogs; +} + +- (IBAction)onSendScreenshotTap:(id)sender +{ + self.sendScreenshot = !self.sendScreenshot; +} + +@end diff --git a/Riot/ViewController/BugReportViewController.xib b/Riot/ViewController/BugReportViewController.xib new file mode 100644 index 000000000..d4ed64587 --- /dev/null +++ b/Riot/ViewController/BugReportViewController.xib @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU +GR4fIyQrLjE3OlUkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw +c1dOU0NvbG9ygAKADRIgwAAAgAOAC1h7MjIsIDIyfdIVChYYWk5TLm9iamVjdHOhF4AEgArSFQoaHaIb +HIAFgAaACRAA0iAKISJfEBROU1RJRkZSZXByZXNlbnRhdGlvboAHgAhPERAuTU0AKgAAB5gAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAADiEXLCliRX8/mGnGTbh97E22fuw/mGnGKWJFfw4hFywAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASLR87OIlfr1C+hPJWzY//VcmM +/1TIi/9UyIv/VcmM/1bNj/9QvoTyOIlfrxItHzsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAEEAgYoYkR/UcKH9ljPkP9VyYv/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VcmL/1jPkP9Rwof2KGJE +fwEEAgYAAAAAAAAAAAAAAAAAAAAAAAAAAAEEAgYyeFOcV8+Q/1XJjP9Uxor/VMaK/1TGiv9Uxor/VMaK +/1TGiv9Uxor/VMaK/1TGiv9Uxor/VcmM/1fPkP8yeFOcAQQCBgAAAAAAAAAAAAAAAAAAAAAoYkR/V8+Q +/1TIi/9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9UyIv/V8+Q +/yhiRH8AAAAAAAAAAAAAAAASLR46UsOH91XJjP9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK +/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1XJjP9Sw4f3Ei0eOgAAAAABAgIEOYlfslfPkP9Uxor/VMaK +/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1PGif9Uxor/UsWJ/1TGiv9Uxor/V8+Q +/zmJX7IBAgIEDSAWK1C/hfRVyYv/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK +/1LFif9XyIz/uenQ/5XfuP9Txon/VMaK/1XJi/9Qv4X0DSAWKyhiRX9WzY//VMaK/1TGiv9Uxor/VMaK +/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1LFif9Xxoz/vOrT///////i9uz/X8uS/1PGif9Uxor/Vs2P +/yhiRX8/mGnGVcmM/1TGiv9Uxor/VMaK/1TGiv9Uxor/U8aJ/1TGiv9Uxor/VMaK/1LFif9XyIz/vOrT +///////p+PH/ctKg/1HFiP9Uxor/VMaK/1XJjP8/mGnGTrd+7VTIiv9Uxor/VMaK/1TGiv9Uxor/U8aK +/1nJj/9RxYn/VMaK/1LFif9XyIz/vOrT///////p+PD/cdGf/0/Eh/9Uxor/VMaK/1TGiv9UyIr/Trd+ +7U22fuxUyIr/VMaK/1TGiv9Uxor/U8aJ/57hv//X8+X/Zs2X/07Ehv9XyIz/wOvV///////n+O//b8+e +/0/Eh/9Uxor/VMaK/1TGiv9Uxor/VMiK/022fuw/mmnGVcmM/1TGiv9Uxor/U8aK/1fIjP/K79z///// +/9nz5v9s0Jz/uurR///////m9+//b9Ge/0/Eh/9Uxor/VMaK/1TGiv9Uxor/VMaK/1XJjP8/mmnGKGJF +f1bNj/9Uxor/VMaK/1TGiv9SxYn/X8qT/8zw3///////9fv3///////n+O//b9Ge/0/Eh/9Uxor/VMaK +/1TGiv9Uxor/VMaK/1TGiv9WzY//KGJFfw0gFitQv4X0VcmL/1TGiv9Uxor/VMaK/1HFiP9eypL/zPDf +///////o+PD/b9Ge/0/Eh/9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9VyYv/UL+F9A0gFisBAgIEOohf +sVfPkP9Uxor/VMaK/1TGiv9Uxor/UcWI/2DLk/+76tH/ddOj/0/Fh/9Uxor/VMaK/1TGiv9Uxor/VMaK +/1TGiv9Uxor/V8+Q/zqIX7EBAgIEAAAAABItHjpSw4f3VcmM/1TGiv9Uxor/VMaK/1TGiv9SxYn/UsWI +/1LFiP9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VcmM/1LDh/cSLR46AAAAAAAAAAAAAAAAKGJE +f1fPkP9UyIv/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9Uxor/VMiL +/1fPkP8oYkR/AAAAAAAAAAAAAAAAAAAAAAEEAgYyeFOcV8+Q/1XJjP9Uxor/VMaK/1TGiv9Uxor/VMaK +/1TGiv9Uxor/VMaK/1TGiv9Uxor/VcmM/1fPkP8yeFOcAQQCBgAAAAAAAAAAAAAAAAAAAAAAAAAAAQQC +BihiRH9Rwof2WM+Q/1XJi/9Uxor/VMaK/1TGiv9Uxor/VMaK/1TGiv9VyYv/WM+Q/1HCh/YoYkR/AQQC +BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEi0fOzqJX69Qv4TzVs2P/1XJjP9UyIv/VMiL +/1XJjP9WzY//UL+E8zqJX68SLR87AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAADiEXLCliRX8/mGnGTbh97E24few/mGnGKWJFfw4hFywAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAA4BAAADAAAAAQAWAAABAQADAAAAAQAWAAABAgADAAAABAAACEYBAwADAAAAAQABAAAB +BgADAAAAAQACAAABEQAEAAAAAQAAAAgBEgADAAAAAQABAAABFQADAAAAAQAEAAABFgADAAAAAQAWAAAB +FwAEAAAAAQAAB5ABHAADAAAAAQABAAABUgADAAAAAQABAAABUwADAAAABAAACE6HcwAHAAAH2AAACFYA +AAAAAAgACAAIAAgAAQABAAEAAQAAB9hhcHBsAiAAAG1udHJSR0IgWFlaIAfZAAIAGQALABoAC2Fjc3BB +UFBMAAAAAGFwcGwAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtYXBwbAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC2Rlc2MAAAEIAAAAb2RzY20AAAF4AAAFnGNwcnQA +AAcUAAAAOHd0cHQAAAdMAAAAFHJYWVoAAAdgAAAAFGdYWVoAAAd0AAAAFGJYWVoAAAeIAAAAFHJUUkMA +AAecAAAADmNoYWQAAAesAAAALGJUUkMAAAecAAAADmdUUkMAAAecAAAADmRlc2MAAAAAAAAAFEdlbmVy +aWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAABRHZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAB8AAAAMc2tTSwAAACgA +AAGEZGFESwAAAC4AAAGsY2FFUwAAACQAAAHadmlWTgAAACQAAAH+cHRCUgAAACYAAAIidWtVQQAAACoA +AAJIZnJGVQAAACgAAAJyaHVIVQAAACgAAAKaemhUVwAAABYAAALCbmJOTwAAACYAAALYY3NDWgAAACIA +AAL+aGVJTAAAAB4AAAMgaXRJVAAAACgAAAM+cm9STwAAACQAAANmZGVERQAAACwAAAOKa29LUgAAABYA +AAO2c3ZTRQAAACYAAALYemhDTgAAABYAAAPMamFKUAAAABoAAAPiZWxHUgAAACIAAAP8cHRQTwAAACYA +AAQebmxOTAAAACgAAAREZXNFUwAAACYAAAQedGhUSAAAACQAAARsdHJUUgAAACIAAASQZmlGSQAAACgA +AASyaHJIUgAAACgAAATacGxQTAAAACwAAAUCcnVSVQAAACIAAAUuYXJFRwAAACYAAAVQZW5VUwAAACYA +AAV2AFYBYQBlAG8AYgBlAGMAbgD9ACAAUgBHAEIAIABwAHIAbwBmAGkAbABHAGUAbgBlAHIAZQBsACAA +UgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABzAGUAUABlAHIAZgBpAGwAIABSAEcAQgAgAGcAZQBuAOgA +cgBpAGMAQx6lAHUAIABoAOwAbgBoACAAUgBHAEIAIABDAGgAdQBuAGcAUABlAHIAZgBpAGwAIABSAEcA +QgAgAEcAZQBuAOkAcgBpAGMAbwQXBDAEMwQwBDsETAQ9BDgEOQAgBD8EQAQ+BEQEMAQ5BDsAIABSAEcA +QgBQAHIAbwBmAGkAbAAgAGcA6QBuAOkAcgBpAHEAdQBlACAAUgBWAEIAwQBsAHQAYQBsAOEAbgBvAHMA +IABSAEcAQgAgAHAAcgBvAGYAaQBskBp1KAAgAFIARwBCACCCcl9pY8+P8ABHAGUAbgBlAHIAaQBzAGsA +IABSAEcAQgAtAHAAcgBvAGYAaQBsAE8AYgBlAGMAbgD9ACAAUgBHAEIAIABwAHIAbwBmAGkAbAXkBegF +1QXkBdkF3AAgAFIARwBCACAF2wXcBdwF2QBQAHIAbwBmAGkAbABvACAAUgBHAEIAIABnAGUAbgBlAHIA +aQBjAG8AUAByAG8AZgBpAGwAIABSAEcAQgAgAGcAZQBuAGUAcgBpAGMAQQBsAGwAZwBlAG0AZQBpAG4A +ZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbMd8vBgAIABSAEcAQgAg1QS4XNMMx3xmbpAaACAAUgBHAEIA +IGPPj/Blh072TgCCLAAgAFIARwBCACAw1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAPAA8EDvwPGA68D +uwAgAFIARwBCAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDpAHIAaQBjAG8AQQBsAGcAZQBtAGUA +ZQBuACAAUgBHAEIALQBwAHIAbwBmAGkAZQBsDkIOGw4jDkQOHw4lDkwAIABSAEcAQgAgDhcOMQ5IDicO +RA4bAEcAZQBuAGUAbAAgAFIARwBCACAAUAByAG8AZgBpAGwAaQBZAGwAZQBpAG4AZQBuACAAUgBHAEIA +LQBwAHIAbwBmAGkAaQBsAGkARwBlAG4AZQByAGkBDQBrAGkAIABSAEcAQgAgAHAAcgBvAGYAaQBsAFUA +bgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBvAGYAaQBsACAAUgBHAEIEHgQxBEkEOAQ5ACAEPwRABD4E +RAQ4BDsETAAgAFIARwBCBkUGRAZBACAGKgY5BjEGSgZBACAAUgBHAEIAIAYnBkQGOQYnBkUARwBlAG4A +ZQByAGkAYwAgAFIARwBCACAAUAByAG8AZgBpAGwAZXRleHQAAAAAQ29weXJpZ2h0IDIwMDcgQXBwbGUg +SW5jLiwgYWxsIHJpZ2h0cyByZXNlcnZlZC4AWFlaIAAAAAAAAPNSAAEAAAABFs9YWVogAAAAAAAAdE0A +AD3uAAAD0FhZWiAAAAAAAABadQAArHMAABc0WFlaIAAAAAAAACgaAAAVnwAAuDZjdXJ2AAAAAAAAAAEB +zQAAc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeSAAD9kf//+6L///2jAAAD3AAAwGzSJSYnKFokY2xhc3Nu +YW1lWCRjbGFzc2VzXxAQTlNCaXRtYXBJbWFnZVJlcKMnKSpaTlNJbWFnZVJlcFhOU09iamVjdNIlJiwt +V05TQXJyYXmiLCrSJSYvMF5OU011dGFibGVBcnJheaMvLCrTMjMKNDU2V05TV2hpdGVcTlNDb2xvclNw +YWNlRDAgMAAQA4AM0iUmODlXTlNDb2xvcqI4KtIlJjs8V05TSW1hZ2WiOypfEA9OU0tleWVkQXJjaGl2 +ZXLRP0BUcm9vdIABAAgAEQAaACMALQAyADcARgBMAFcAXgBlAHIAeQCBAIMAhQCKAIwAjgCXAJwApwCp +AKsArQCyALUAtwC5ALsAvQDCANkA2wDdEQ8RFBEfESgROxE/EUoRUxFYEWARYxFoEXcRexGCEYoRlxGc +EZ4RoBGlEa0RsBG1Eb0RwBHSEdUR2gAAAAAAAAIBAAAAAAAAAEEAAAAAAAAAAAAAAAAAABHcA + + + + diff --git a/Riot/ViewController/CallViewController.m b/Riot/ViewController/CallViewController.m index 07380eb21..8075d22ac 100644 --- a/Riot/ViewController/CallViewController.m +++ b/Riot/ViewController/CallViewController.m @@ -19,10 +19,7 @@ #import "AppDelegate.h" -#import "RageShakeManager.h" - #import "AvatarGenerator.h" -#import "RiotDesignValues.h" #import "MXRoom+Riot.h" diff --git a/Riot/ViewController/ContactDetailsViewController.m b/Riot/ViewController/ContactDetailsViewController.m index 504b74d72..2e349bb89 100644 --- a/Riot/ViewController/ContactDetailsViewController.m +++ b/Riot/ViewController/ContactDetailsViewController.m @@ -21,10 +21,6 @@ #import "RoomMemberTitleView.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import "AvatarGenerator.h" #import "Tools.h" @@ -256,6 +252,21 @@ self.bottomImageView.hidden = YES; } +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + // Restore navigation bar display + [self hideNavigationBarBorder:NO]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(coordinator.transitionDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + + // Hide the bottom border of the navigation bar + [self hideNavigationBarBorder:YES]; + + }); +} + - (void)destroy { [super destroy]; diff --git a/Riot/ViewController/ContactsTableViewController.h b/Riot/ViewController/ContactsTableViewController.h index ead3c02a9..8f1034b65 100644 --- a/Riot/ViewController/ContactsTableViewController.h +++ b/Riot/ViewController/ContactsTableViewController.h @@ -15,10 +15,8 @@ limitations under the License. */ -#import - +#import "ContactsDataSource.h" #import "ContactTableViewCell.h" -#import "RiotDesignValues.h" @class ContactsTableViewController; @@ -41,24 +39,10 @@ 'ContactsTableViewController' instance is used to display/filter a list of contacts. See 'ContactsTableViewController-inherited' object for example of use. */ -@interface ContactsTableViewController : MXKViewController +@interface ContactsTableViewController : MXKViewController { @protected - // Section indexes - NSInteger searchInputSection; - NSInteger filteredLocalContactsSection; - NSInteger filteredMatrixContactsSection; - - // The contact used to describe the current user. - MXKContact *userContact; - - // Tell whether the non-matrix-enabled contacts must be hidden or not. NO by default. - BOOL hideNonMatrixEnabledContacts; - - // Search results - NSString *currentSearchText; - NSMutableArray *filteredLocalContacts; - NSMutableArray *filteredMatrixContacts; + ContactsDataSource *contactsDataSource; } /** @@ -80,50 +64,42 @@ /** The contacts table view. */ -@property (weak, nonatomic) IBOutlet UITableView *tableView; +@property (weak, nonatomic) IBOutlet UITableView *contactsTableView; /** - Tell whether the matrix id should be added by default in the matrix contact display name (NO by default). - If NO, the matrix id is added only to disambiguate the contact display names which appear several times. + If YES, the table view will scroll at the top on the next data source refresh. + It comes back to NO after each refresh. */ -@property (nonatomic) BOOL forceMatrixIdInDisplayName; +@property (nonatomic) BOOL shouldScrollToTopOnRefresh; /** - The type of standard accessory view the contact cells should use - Default is UITableViewCellAccessoryNone. + The Google Analytics Instance screen name (Default is "ContactsTable"). */ -@property (nonatomic) UITableViewCellAccessoryType contactCellAccessoryType; +@property (nonatomic) NSString *screenName; /** - An image used to create a custom accessy view on the right side of the contact cells. - If set, use custom view. ignore accessoryType - */ -@property (nonatomic) UIImage *contactCellAccessoryImage; - -/** - The dictionary of the ignored local contacts, the keys are their email. Empty by default. - */ -@property (nonatomic) NSMutableDictionary *ignoredContactsByEmail; - -/** - The dictionary of the ignored matrix contacts, the keys are their matrix identifier. Empty by default. - */ -@property (nonatomic) NSMutableDictionary *ignoredContactsByMatrixId; - -/** - Filter the contacts list, by keeping only the contacts who have the search pattern - as prefix in their display name, their matrix identifiers and/or their contact methods (emails, phones). + Refresh the cell selection in the table. - @param searchText the search pattern (nil to reset filtering). - @param forceReset tell whether the search request must be applied by ignoring the previous search result if any (use NO by default). - @param complete a block object called when the operation is complete. + This must be done accordingly to the currently selected contact in the master tabbar of the application. + + @param forceVisible if YES and if the corresponding cell is not visible, scroll the table view to make it visible. */ -- (void)searchWithPattern:(NSString *)searchText forceReset:(BOOL)forceReset complete:(void (^)())complete; +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible; + +/** + Display the contacts described in the provided data source. + + The provided data source will replace the current data source if any. The caller + should dispose properly this data source if it is not used anymore. + + @param listDataSource the data source providing the contacts list. + */ +- (void)displayList:(ContactsDataSource*)listDataSource; /** Refresh the contacts table display. */ -- (void)refreshTableView; +- (void)refreshContactsTable; /** The delegate for the view controller. diff --git a/Riot/ViewController/ContactsTableViewController.m b/Riot/ViewController/ContactsTableViewController.m index 6bce5ede6..83df3d870 100644 --- a/Riot/ViewController/ContactsTableViewController.m +++ b/Riot/ViewController/ContactsTableViewController.m @@ -19,8 +19,6 @@ #import "UIViewController+RiotSearch.h" -#import "RageShakeManager.h" - #import "AppDelegate.h" #define CONTACTS_TABLEVC_LOCALCONTACTS_BITWISE 0x01 @@ -31,26 +29,8 @@ @interface ContactsTableViewController () { - // Search processing - dispatch_queue_t searchProcessingQueue; - NSUInteger searchProcessingCount; - NSString *searchProcessingText; - NSMutableArray *searchProcessingLocalContacts; - NSMutableArray *searchProcessingMatrixContacts; - // Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. id kAppDelegateDidTapStatusBarNotificationObserver; - - BOOL forceSearchResultRefresh; - - // This dictionary tells for each display name whether it appears several times. - NSMutableDictionary *isMultiUseNameByDisplayName; - - // Shrinked sections. - NSInteger shrinkedSectionsBitMask; - - UIView *localContactsCheckboxContainer; - UIImageView *localContactsCheckbox; } @end @@ -82,23 +62,7 @@ self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; - // Prepare search session - searchProcessingQueue = dispatch_queue_create("ContactsTableViewController", DISPATCH_QUEUE_SERIAL); - searchProcessingCount = 0; - searchProcessingText = nil; - searchProcessingLocalContacts = nil; - searchProcessingMatrixContacts = nil; - - _ignoredContactsByEmail = [NSMutableDictionary dictionary]; - _ignoredContactsByMatrixId = [NSMutableDictionary dictionary]; - - isMultiUseNameByDisplayName = [NSMutableDictionary dictionary]; - - _forceMatrixIdInDisplayName = NO; - - shrinkedSectionsBitMask = 0; - - hideNonMatrixEnabledContacts = NO; + _screenName = @"ContactsTable"; } - (void)viewDidLoad @@ -107,14 +71,20 @@ // Do any additional setup after loading the view, typically from a nib. // Check whether the view controller has been pushed via storyboard - if (!self.tableView) + if (!self.contactsTableView) { // Instantiate view controller objects [[[self class] nib] instantiateWithOwner:self options:nil]; } + // Finalize table view configuration + self.contactsTableView.delegate = self; + self.contactsTableView.dataSource = contactsDataSource; // Note: dataSource may be nil here + + [self.contactsTableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:ContactTableViewCell.defaultReuseIdentifier]; + // Hide line separators of empty cells - self.tableView.tableFooterView = [[UIView alloc] init]; + self.contactsTableView.tableFooterView = [[UIView alloc] init]; } - (void)didReceiveMemoryWarning @@ -125,39 +95,9 @@ - (void)destroy { - filteredLocalContacts = nil; - filteredMatrixContacts = nil; - - _ignoredContactsByEmail = nil; - _ignoredContactsByMatrixId = nil; - - userContact = nil; - - forceSearchResultRefresh = NO; - - searchProcessingQueue = nil; - searchProcessingLocalContacts = nil; - searchProcessingMatrixContacts = nil; - - isMultiUseNameByDisplayName = nil; - - _contactCellAccessoryImage = nil; - - localContactsCheckboxContainer = nil; - localContactsCheckbox = nil; - [super destroy]; } -- (void)addMatrixSession:(MXSession *)mxSession -{ - [super addMatrixSession:mxSession]; - - // FIXME: Handle multi accounts - NSString *displayName = NSLocalizedStringFromTable(@"you", @"Vector", nil); - userContact = [[MXKContact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:self.mainSession.myUser.userId]; -} - - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; @@ -166,22 +106,10 @@ id tracker = [[GAI sharedInstance] defaultTracker]; if (tracker) { - [tracker set:kGAIScreenName value:@"ContactsTable"]; + [tracker set:kGAIScreenName value:_screenName]; [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; } - - // Observe kAppDelegateDidTapStatusBarNotification. - kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self.tableView setContentOffset:CGPointMake(-self.tableView.contentInset.left, -self.tableView.contentInset.top) animated:YES]; - - }]; - - // Register on contact update notifications - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactManagerDidUpdate:) name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil]; - + // Check whether the access to the local contacts has not been already asked. if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) { @@ -190,14 +118,15 @@ // ask user permission to access their local contacts. [MXKAppSettings standardAppSettings].syncLocalContacts = YES; } - else - { - // Refresh the matrix identifiers for all the local contacts. - [[MXKContactManager sharedManager] updateMatrixIDsForAllLocalContacts]; - } + + // Observe kAppDelegateDidTapStatusBarNotification. + kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + + [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.contentInset.left, -self.contactsTableView.contentInset.top) animated:YES]; + + }]; - // Scroll to the top the current table content if any - [self.tableView setContentOffset:CGPointMake(-self.tableView.contentInset.left, -self.tableView.contentInset.top) animated:NO]; + [self refreshContactsTable]; } - (void)viewWillDisappear:(BOOL)animated @@ -209,742 +138,151 @@ [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver]; kAppDelegateDidTapStatusBarNotificationObserver = nil; } - - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil]; } #pragma mark - -- (void)setForceMatrixIdInDisplayName:(BOOL)forceMatrixIdInDisplayName +- (void)displayList:(ContactsDataSource*)listDataSource { - if (_forceMatrixIdInDisplayName != forceMatrixIdInDisplayName) + // Cancel registration on existing dataSource if any + if (contactsDataSource) { - _forceMatrixIdInDisplayName = forceMatrixIdInDisplayName; - - if (self.tableView) - { - [self refreshTableView]; - } + contactsDataSource.delegate = nil; + } + + contactsDataSource = listDataSource; + contactsDataSource.delegate = self; + + if (self.contactsTableView) + { + // Set up table data source + self.contactsTableView.dataSource = contactsDataSource; } } -- (void)searchWithPattern:(NSString *)searchText forceReset:(BOOL)forceRefresh complete:(void (^)())complete +- (void)refreshContactsTable { - // Update search results. - searchText = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [self.contactsTableView reloadData]; - searchProcessingCount++; - [self startActivityIndicator]; - - dispatch_async(searchProcessingQueue, ^{ - - if (!searchText.length) - { - searchProcessingLocalContacts = nil; - searchProcessingMatrixContacts = nil; - - // Disclose by default the sections if a search was in progress. - if (searchProcessingText.length) - { - shrinkedSectionsBitMask = 0; - } - } - else if (forceRefresh || !searchProcessingText.length || [searchText hasPrefix:searchProcessingText] == NO) - { - // Retrieve all the local contacts - searchProcessingLocalContacts = [self unfilteredLocalContactsArray]; - - // Retrieve all known matrix users - searchProcessingMatrixContacts = [self unfilteredMatrixContactsArray]; - - // Disclose the sections - shrinkedSectionsBitMask = 0; - } - - for (NSUInteger index = 0; index < searchProcessingLocalContacts.count;) - { - MXKContact* contact = searchProcessingLocalContacts[index]; - - if (![contact hasPrefix:searchText]) - { - [searchProcessingLocalContacts removeObjectAtIndex:index]; - } - else - { - // Next - index++; - } - } - - for (NSUInteger index = 0; index < searchProcessingMatrixContacts.count;) - { - MXKContact* contact = searchProcessingMatrixContacts[index]; - - if (![contact hasPrefix:searchText]) - { - [searchProcessingMatrixContacts removeObjectAtIndex:index]; - } - else - { - // Next - index++; - } - } - - // Sort the refreshed list of the invitable contacts - [[MXKContactManager sharedManager] sortAlphabeticallyContacts:searchProcessingLocalContacts]; - [[MXKContactManager sharedManager] sortContactsByLastActiveInformation:searchProcessingMatrixContacts]; - - searchProcessingText = searchText; - - dispatch_sync(dispatch_get_main_queue(), ^{ - - // Sanity check: check whether self has been destroyed. - if (!searchProcessingQueue) - { - return; - } - - // Render the search result only if there is no other search in progress. - searchProcessingCount --; - - if (!searchProcessingCount) - { - if (!forceSearchResultRefresh) - { - [self stopActivityIndicator]; - - // Scroll the resulting list to the top only when the search pattern has been modified. - BOOL shouldScrollToTop = (currentSearchText != searchProcessingText); - - // Update the filtered contacts. - currentSearchText = searchProcessingText; - filteredLocalContacts = searchProcessingLocalContacts; - filteredMatrixContacts = searchProcessingMatrixContacts; - - if (!self.forceMatrixIdInDisplayName) - { - [isMultiUseNameByDisplayName removeAllObjects]; - for (MXKContact* contact in filteredMatrixContacts) - { - isMultiUseNameByDisplayName[contact.displayName] = (isMultiUseNameByDisplayName[contact.displayName] ? @(YES) : @(NO)); - } - } - - // Refresh display - [self refreshTableView]; - - if (shouldScrollToTop) - { - // Scroll to the top - [self.tableView setContentOffset:CGPointMake(-self.tableView.contentInset.left, -self.tableView.contentInset.top) animated:NO]; - } - - if (complete) - { - complete(); - } - } - else - { - // Launch a new search - forceSearchResultRefresh = NO; - [self searchWithPattern:searchProcessingText forceReset:YES complete:complete]; - } - } - }); - - }); -} - -- (void)refreshTableView -{ - [self.tableView reloadData]; -} - -#pragma mark - Internals - -- (void)onContactManagerDidUpdate:(NSNotification *)notif -{ - // Check whether a search is in progress - if (searchProcessingCount) + if (_shouldScrollToTopOnRefresh) { - forceSearchResultRefresh = YES; - return; + [self scrollToTop:NO]; + _shouldScrollToTopOnRefresh = NO; } - // Refresh the search result - [self searchWithPattern:currentSearchText forceReset:YES complete:nil]; + // In case of split view controller where the primary and secondary view controllers are displayed side-by-side on screen, + // the selected room (if any) is updated and kept visible. + // Note: 'isCollapsed' property is available in UISplitViewController for iOS 8 and later. + if (self.splitViewController && (![self.splitViewController respondsToSelector:@selector(isCollapsed)] || !self.splitViewController.isCollapsed)) + { + [self refreshCurrentSelectedCell:YES]; + } } -- (NSMutableArray*)unfilteredLocalContactsArray +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible { - // Retrieve all the contacts obtained by splitting each local contact by contact method. This list is ordered alphabetically. - NSMutableArray *unfilteredLocalContacts = [NSMutableArray arrayWithArray:[MXKContactManager sharedManager].localContactsSplitByContactMethod]; - - // Remove the ignored contacts - // + Check whether the non-matrix-enabled contacts must be ignored - for (NSUInteger index = 0; index < unfilteredLocalContacts.count;) + // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. + NSIndexPath *currentSelectedCellIndexPath = nil; + MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; + if (masterTabBarController.currentContactDetailViewController) { - MXKContact* contact = unfilteredLocalContacts[index]; - - NSArray *identifiers = contact.matrixIdentifiers; - if (identifiers.count) - { - if ([_ignoredContactsByMatrixId objectForKey:identifiers.firstObject]) - { - [unfilteredLocalContacts removeObjectAtIndex:index]; - continue; - } - } - else if (hideNonMatrixEnabledContacts) - { - // Ignore non-matrix-enabled contact - [unfilteredLocalContacts removeObjectAtIndex:index]; - continue; - } - else - { - NSArray *emails = contact.emailAddresses; - if (emails.count) - { - // Here the contact has only one email address. - MXKEmail *email = emails.firstObject; - - // Trick: ignore @facebook.com email addresses from the results - facebook have discontinued that service... - if ([_ignoredContactsByEmail objectForKey:email.emailAddress] || [email.emailAddress hasSuffix:@"@facebook.com"]) - { - [unfilteredLocalContacts removeObjectAtIndex:index]; - continue; - } - } - else - { - // The contact has here a phone number. - // Ignore this contact if the phone number is not linked to a matrix id because the invitation by SMS is not supported yet. - MXKPhoneNumber *phoneNumber = contact.phoneNumbers.firstObject; - if (!phoneNumber.matrixID) - { - [unfilteredLocalContacts removeObjectAtIndex:index]; - continue; - } - } - } - - index++; + // Look for the rank of this selected contact in displayed recents + currentSelectedCellIndexPath = [contactsDataSource cellIndexPathWithContact:masterTabBarController.selectedContact]; } - return unfilteredLocalContacts; -} - -- (NSMutableArray*)unfilteredMatrixContactsArray -{ - NSArray *matrixContacts = [MXKContactManager sharedManager].matrixContacts; - NSMutableArray *unfilteredMatrixContacts = [NSMutableArray arrayWithCapacity:matrixContacts.count]; - - // Matrix ids: split contacts with several ids, and remove the current participants. - for (MXKContact* contact in matrixContacts) + if (currentSelectedCellIndexPath) { - NSArray *identifiers = contact.matrixIdentifiers; - if (identifiers.count > 1) + // Select the right row + [self.contactsTableView selectRowAtIndexPath:currentSelectedCellIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; + + if (forceVisible) { - for (NSString *userId in identifiers) - { - if ([_ignoredContactsByMatrixId objectForKey:userId] == nil) - { - MXKContact *splitContact = [[MXKContact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; - [unfilteredMatrixContacts addObject:splitContact]; - } - } + // Scroll table view to make the selected row appear at second position + NSInteger topCellIndexPathRow = currentSelectedCellIndexPath.row ? currentSelectedCellIndexPath.row - 1: currentSelectedCellIndexPath.row; + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:currentSelectedCellIndexPath.section]; + [self.contactsTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; } - else if (identifiers.count) - { - NSString *userId = identifiers.firstObject; - if ([_ignoredContactsByMatrixId objectForKey:userId] == nil) - { - [unfilteredMatrixContacts addObject:contact]; - } - } - } - - return unfilteredMatrixContacts; -} - -#pragma mark - UITableView data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - NSInteger count = 0; - - searchInputSection = filteredLocalContactsSection = filteredMatrixContactsSection = -1; - - if (currentSearchText.length) - { - searchInputSection = count++; } else { - // Display by default the full address book ordered alphabetically, mixing Matrix enabled and non-Matrix enabled users. - if (!filteredLocalContacts) + NSIndexPath *indexPath = [self.contactsTableView indexPathForSelectedRow]; + if (indexPath) { - filteredLocalContacts = [self unfilteredLocalContactsArray]; + [self.contactsTableView deselectRowAtIndexPath:indexPath animated:NO]; } } - - // Keep visible the header for the both contact sections, even if their are empty. - filteredLocalContactsSection = count++; - filteredMatrixContactsSection = count++; - - return count; } -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +#pragma mark - MXKDataSourceDelegate + +- (Class)cellViewClassForCellData:(MXKCellData*)cellData { - NSInteger count = 0; - - if (section == searchInputSection) + if ([cellData isKindOfClass:MXKContact.class]) { - count = 1; - } - else if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTS_TABLEVC_LOCALCONTACTS_BITWISE)) - { - count = filteredLocalContacts.count; - } - else if (section == filteredMatrixContactsSection && !(shrinkedSectionsBitMask & CONTACTS_TABLEVC_KNOWNCONTACTS_BITWISE)) - { - if (currentSearchText.length) - { - count = filteredMatrixContacts.count; - } - else - { - // Display a message to invite the user to use the search field. - count = 1; - } + return ContactTableViewCell.class; } - return count; + return nil; } -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData { - // Consider first the case of the known contacts section when no search is in progress. - if (!currentSearchText.length && indexPath.section == filteredMatrixContactsSection && indexPath.row == 0) + if ([cellData isKindOfClass:MXKContact.class]) { - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"defaultKnownContactCell"]; - if (!cell) - { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"defaultKnownContactCell"]; - } - - cell.textLabel.text = NSLocalizedStringFromTable(@"contacts_matrix_users_search_prompt", @"Vector", nil); - cell.textLabel.numberOfLines = 2; - cell.textLabel.textColor = kRiotTextColorGray; - cell.textLabel.font = [UIFont systemFontOfSize:15.0]; - cell.selectionStyle = UITableViewCellSelectionStyleNone; - return cell; + return [ContactTableViewCell defaultReuseIdentifier]; } - // Prepare a contact cell here - ContactTableViewCell* contactCell = [tableView dequeueReusableCellWithIdentifier:[ContactTableViewCell defaultReuseIdentifier]]; - - if (!contactCell) - { - contactCell = [[ContactTableViewCell alloc] init]; - } - else - { - // Restore default values - contactCell.contentView.alpha = 1; - contactCell.userInteractionEnabled = YES; - contactCell.accessoryType = UITableViewCellAccessoryNone; - contactCell.accessoryView = nil; - } - - MXKContact *contact; - - if (indexPath.section == searchInputSection) - { - // Show what the user is typing in a cell. So that he can click on it - contact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; - - contactCell.selectionStyle = UITableViewCellSelectionStyleDefault; - } - else if (indexPath.section == filteredLocalContactsSection) - { - if (indexPath.row < filteredLocalContacts.count) - { - contact = filteredLocalContacts[indexPath.row]; - - contactCell.selectionStyle = UITableViewCellSelectionStyleDefault; - contactCell.showMatrixIdInDisplayName = YES; - } - } - else if (indexPath.section == filteredMatrixContactsSection) - { - if (indexPath.row < filteredMatrixContacts.count) - { - contact = filteredMatrixContacts[indexPath.row]; - - contactCell.selectionStyle = UITableViewCellSelectionStyleDefault; - contactCell.showMatrixIdInDisplayName = self.forceMatrixIdInDisplayName ? YES : [isMultiUseNameByDisplayName[contact.displayName] isEqualToNumber:@(YES)]; - } - } - - if (contact) - { - [contactCell render:contact]; - - // The search displays contacts to invite. - if (indexPath.section == filteredLocalContactsSection || indexPath.section == filteredMatrixContactsSection) - { - // Add the right accessory view if any - contactCell.accessoryType = self.contactCellAccessoryType; - if (self.contactCellAccessoryImage) - { - contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage]; - } - - } - else if (indexPath.section == searchInputSection) - { - // This is the text entered by the user - // Check whether the search input is a valid email or a Matrix user ID before adding the accessory view. - if (![MXTools isEmailAddress:currentSearchText] && ![MXTools isMatrixUserIdentifier:currentSearchText]) - { - contactCell.contentView.alpha = 0.5; - contactCell.userInteractionEnabled = NO; - } - else - { - // Add the right accessory view if any - contactCell.accessoryType = self.contactCellAccessoryType; - if (self.contactCellAccessoryImage) - { - contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage]; - } - } - } - } - - return contactCell; + return nil; } -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes { - return NO; + [self refreshContactsTable]; +} + +#pragma mark - Internal methods + +- (void)scrollToTop:(BOOL)animated +{ + // Scroll to the top + [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.contentInset.left, -self.contactsTableView.contentInset.top) animated:animated]; } #pragma mark - UITableView delegate - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - if (section == filteredLocalContactsSection || section == filteredMatrixContactsSection) - { - if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTS_TABLEVC_LOCALCONTACTS_BITWISE)) - { - return CONTACTS_TABLEVC_LOCALCONTACTS_SECTION_HEADER_HEIGHT; - } - - return CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT; - } - return 0; + return [contactsDataSource heightForHeaderInSection:section]; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { - UIView* sectionHeader; - - CGFloat height = [self tableView:tableView heightForHeaderInSection:section]; - if (height != 0) - { - NSInteger sectionBitwise = -1; - - sectionHeader = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, height)]; - sectionHeader.backgroundColor = kRiotColorLightGrey; - - CGRect frame = sectionHeader.frame; - frame.origin.x = 20; - frame.origin.y = 5; - frame.size.width = sectionHeader.frame.size.width - 10; - frame.size.height = 20; - UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame]; - headerLabel.font = [UIFont boldSystemFontOfSize:15.0]; - headerLabel.backgroundColor = [UIColor clearColor]; - [sectionHeader addSubview:headerLabel]; - - if (section == filteredLocalContactsSection) - { - headerLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"contacts_address_book_section", @"Vector", nil), filteredLocalContacts.count]; - - sectionBitwise = CONTACTS_TABLEVC_LOCALCONTACTS_BITWISE; - } - else //if (section == filteredMatrixContactsSection) - { - if (currentSearchText.length) - { - headerLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"contacts_matrix_users_section", @"Vector", nil), filteredMatrixContacts.count]; - - // This section is collapsable only if it is not empty - if (filteredMatrixContacts.count) - { - sectionBitwise = CONTACTS_TABLEVC_KNOWNCONTACTS_BITWISE; - } - } - else - { - headerLabel.text = NSLocalizedStringFromTable(@"contacts_matrix_users_default_section", @"Vector", nil); - } - } - - if (sectionBitwise != -1) - { - // Add shrink button - UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom]; - frame = sectionHeader.frame; - frame.origin.x = frame.origin.y = 0; - frame.size.height = CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT; - shrinkButton.frame = frame; - shrinkButton.backgroundColor = [UIColor clearColor]; - [shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - shrinkButton.tag = sectionBitwise; - [sectionHeader addSubview:shrinkButton]; - sectionHeader.userInteractionEnabled = YES; - - // Add shrink icon - UIImage *chevron; - if (shrinkedSectionsBitMask & sectionBitwise) - { - chevron = [UIImage imageNamed:@"disclosure_icon"]; - } - else - { - chevron = [UIImage imageNamed:@"shrink_icon"]; - } - UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron]; - chevronView.contentMode = UIViewContentModeCenter; - frame = chevronView.frame; - frame.origin.x = shrinkButton.frame.size.width - frame.size.width - 16; - frame.origin.y = (shrinkButton.frame.size.height - frame.size.height) / 2; - chevronView.frame = frame; - [sectionHeader addSubview:chevronView]; - chevronView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin); - } - - if (section == filteredLocalContactsSection && !(shrinkedSectionsBitMask & CONTACTS_TABLEVC_LOCALCONTACTS_BITWISE)) - { - NSLayoutConstraint *leadingConstraint, *trailingConstraint, *topConstraint, *bottomConstraint; - NSLayoutConstraint *widthConstraint, *heightConstraint, *centerYConstraint; - - if (!localContactsCheckboxContainer) - { - CGFloat containerWidth = sectionHeader.frame.size.width; - - localContactsCheckboxContainer = [[UIView alloc] initWithFrame:CGRectMake(0, CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT, containerWidth, sectionHeader.frame.size.height - CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT)]; - localContactsCheckboxContainer.backgroundColor = [UIColor clearColor]; - localContactsCheckboxContainer.translatesAutoresizingMaskIntoConstraints = NO; - - // Add Checkbox and Label - localContactsCheckbox = [[UIImageView alloc] initWithFrame:CGRectMake(23, 5, 22, 22)]; - localContactsCheckbox.translatesAutoresizingMaskIntoConstraints = NO; - [localContactsCheckboxContainer addSubview:localContactsCheckbox]; - - UILabel *checkboxLabel = [[UILabel alloc] initWithFrame:CGRectMake(54, 5, containerWidth - 64, 30)]; - checkboxLabel.translatesAutoresizingMaskIntoConstraints = NO; - checkboxLabel.textColor = kRiotTextColorBlack; - checkboxLabel.font = [UIFont systemFontOfSize:16.0]; - checkboxLabel.text = NSLocalizedStringFromTable(@"contacts_address_book_matrix_users_toggle", @"Vector", nil); - [localContactsCheckboxContainer addSubview:checkboxLabel]; - - UIView *checkboxMask = [[UIView alloc] initWithFrame:CGRectMake(16, -2, 36, 36)]; - checkboxMask.translatesAutoresizingMaskIntoConstraints = NO; - [localContactsCheckboxContainer addSubview:checkboxMask]; - // Listen to check box tap - checkboxMask.userInteractionEnabled = YES; - UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCheckBoxTap:)]; - [tapGesture setNumberOfTouchesRequired:1]; - [tapGesture setNumberOfTapsRequired:1]; - [tapGesture setDelegate:self]; - [checkboxMask addGestureRecognizer:tapGesture]; - - // Add switch constraints - leadingConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeLeading - multiplier:1 - constant:23]; - - topConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeTop - multiplier:1 - constant:5]; - - widthConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:22]; - heightConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckbox - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:22]; - - [NSLayoutConstraint activateConstraints:@[leadingConstraint, topConstraint, widthConstraint, heightConstraint]]; - - - // Add Label constraints - centerYConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel - attribute:NSLayoutAttributeCenterY - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckbox - attribute:NSLayoutAttributeCenterY - multiplier:1 - constant:0.0f]; - heightConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:30]; - leadingConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckbox - attribute:NSLayoutAttributeTrailing - multiplier:1 - constant:10]; - trailingConstraint = [NSLayoutConstraint constraintWithItem:checkboxLabel - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeTrailing - multiplier:1 - constant:-10]; - - [NSLayoutConstraint activateConstraints:@[centerYConstraint, heightConstraint, leadingConstraint, trailingConstraint]]; - - // Add check box mask constraints - heightConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:36]; - - centerYConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask - attribute:NSLayoutAttributeCenterY - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckbox - attribute:NSLayoutAttributeCenterY - multiplier:1 - constant:0.0f]; - - leadingConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:localContactsCheckbox - attribute:NSLayoutAttributeLeading - multiplier:1 - constant:-7]; - - trailingConstraint = [NSLayoutConstraint constraintWithItem:checkboxMask - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:checkboxLabel - attribute:NSLayoutAttributeTrailing - multiplier:1 - constant:0]; - - [NSLayoutConstraint activateConstraints:@[heightConstraint, centerYConstraint, leadingConstraint, trailingConstraint]]; - } - - // Set the right value of the tick box - localContactsCheckbox.image = hideNonMatrixEnabledContacts ? [UIImage imageNamed:@"selection_tick"] : [UIImage imageNamed:@"selection_untick"]; - - // Add the check box container - [sectionHeader addSubview:localContactsCheckboxContainer]; - leadingConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:sectionHeader - attribute:NSLayoutAttributeLeading - multiplier:1 - constant:0]; - widthConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:sectionHeader - attribute:NSLayoutAttributeWidth - multiplier:1 - constant:0]; - topConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:sectionHeader - attribute:NSLayoutAttributeTop - multiplier:1 - constant:CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT]; - bottomConstraint = [NSLayoutConstraint constraintWithItem:localContactsCheckboxContainer - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:sectionHeader - attribute:NSLayoutAttributeBottom - multiplier:1 - constant:0]; - - [NSLayoutConstraint activateConstraints:@[leadingConstraint, widthConstraint, topConstraint, bottomConstraint]]; - } - } - return sectionHeader; + return [contactsDataSource viewForHeaderInSection:section withFrame:[tableView rectForHeaderInSection:section]]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - if (!currentSearchText.length && indexPath.section == filteredMatrixContactsSection && indexPath.row == 0) + if ([contactsDataSource contactAtIndexPath:indexPath]) { - return 50; + // Return the default height of the contact cell + return 74.0; } - return 74.0; + return 50; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (self.contactsTableViewControllerDelegate) { - NSInteger row = indexPath.row; - MXKContact *mxkContact; - - if (indexPath.section == searchInputSection) - { - mxkContact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil]; - } - else if (indexPath.section == filteredLocalContactsSection) - { - mxkContact = filteredLocalContacts[row]; - } - else if (indexPath.section == filteredMatrixContactsSection) - { - mxkContact = filteredMatrixContacts[row]; - } + MXKContact *mxkContact = [contactsDataSource contactAtIndexPath:indexPath]; if (mxkContact) { [self.contactsTableViewControllerDelegate contactsTableViewController:self didSelectContact:mxkContact]; + + // Keep selected the cell by default. + return; } } // Else do nothing by default - `ContactsTableViewController-inherited` instance must override this method. @@ -956,23 +294,22 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { - [self searchWithPattern:searchText forceReset:NO complete:nil]; + [contactsDataSource searchWithPattern:searchText forceReset:NO]; } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { // "Done" key has been pressed. - // Check whether the current search input is a valid email or a Matrix user ID - if (currentSearchText.length && ([MXTools isEmailAddress:currentSearchText] || [MXTools isMatrixUserIdentifier:currentSearchText])) + if (self.contactsTableViewControllerDelegate) { - // Select the contact related to the search input, rather than having to hit + - if (searchInputSection != -1) + // Check whether the current search input is a valid email or a Matrix user ID + MXKContact* filedContact = [contactsDataSource searchInputContact]; + if (filedContact) { - [self tableView:self.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:searchInputSection]]; - return; + // Select the contact related to the search input, rather than having to hit + + [self.contactsTableViewControllerDelegate contactsTableViewController:self didSelectContact:filedContact]; } - } // Dismiss keyboard @@ -984,7 +321,7 @@ searchBar.text = nil; // Reset filtering - [self searchWithPattern:nil forceReset:NO complete:nil]; + [contactsDataSource searchWithPattern:nil forceReset:NO]; // Leave search [searchBar resignFirstResponder]; @@ -992,71 +329,4 @@ [self withdrawViewControllerAnimated:YES completion:nil]; } -#pragma mark - Action - -- (IBAction)onButtonPressed:(id)sender -{ - if ([sender isKindOfClass:[UIButton class]]) - { - UIButton *shrinkButton = (UIButton*)sender; - NSInteger selectedSectionBit = shrinkButton.tag; - - if (shrinkedSectionsBitMask & selectedSectionBit) - { - // Disclose the section - shrinkedSectionsBitMask &= ~selectedSectionBit; - } - else - { - // Shrink this section - shrinkedSectionsBitMask |= selectedSectionBit; - } - - // Refresh - [self refreshTableView]; - } -} - -#pragma mark - Action - -- (IBAction)onCheckBoxTap:(UITapGestureRecognizer*)sender -{ - // Update local contacts filter - hideNonMatrixEnabledContacts = !hideNonMatrixEnabledContacts; - - // Check whether a search is in progress - if (searchProcessingCount) - { - forceSearchResultRefresh = YES; - return; - } - - // Refresh the search result - if (hideNonMatrixEnabledContacts) - { - // Remove the non-matrix-enabled contacts from the current filtered local contacts - for (NSUInteger index = 0; index < filteredLocalContacts.count;) - { - MXKContact* contact = filteredLocalContacts[index]; - - NSArray *identifiers = contact.matrixIdentifiers; - if (!identifiers.count) - { - [filteredLocalContacts removeObjectAtIndex:index]; - continue; - } - - index++; - } - - // Refresh display - [self refreshTableView]; - } - else - { - // Refresh the search result by launching a new search session. - [self searchWithPattern:currentSearchText forceReset:YES complete:nil]; - } -} - @end diff --git a/Riot/ViewController/ContactsTableViewController.xib b/Riot/ViewController/ContactsTableViewController.xib index 4d6297107..90bae50e3 100644 --- a/Riot/ViewController/ContactsTableViewController.xib +++ b/Riot/ViewController/ContactsTableViewController.xib @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + @@ -27,7 +27,6 @@ - diff --git a/Riot/ViewController/CountryPickerViewController.m b/Riot/ViewController/CountryPickerViewController.m index 1eb600692..87237af0c 100644 --- a/Riot/ViewController/CountryPickerViewController.m +++ b/Riot/ViewController/CountryPickerViewController.m @@ -18,10 +18,6 @@ #import "AppDelegate.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - @implementation CountryPickerViewController - (void)finalizeInit diff --git a/Riot/ViewController/DirectoryServerPickerViewController.h b/Riot/ViewController/DirectoryServerPickerViewController.h new file mode 100644 index 000000000..e4f3edbe4 --- /dev/null +++ b/Riot/ViewController/DirectoryServerPickerViewController.h @@ -0,0 +1,35 @@ +/* + Copyright 2017 Vector Creations 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 DirectoryServerPickerViewController : MXKTableViewController + +/** + Display data managed by the passed `MXKDirectoryServersDataSource`. + + @param dataSource the data source serving the data. + @param onComplete a block called when the picker disappears. It provides data about + the selected protocol instance or homeserver. + Both nil means the user cancelled the picker. + */ +- (void)displayWithDataSource:(MXKDirectoryServersDataSource*)dataSource + onComplete:(void (^)(id cellData))onComplete; + +@end + diff --git a/Riot/ViewController/DirectoryServerPickerViewController.m b/Riot/ViewController/DirectoryServerPickerViewController.m new file mode 100644 index 000000000..25001f157 --- /dev/null +++ b/Riot/ViewController/DirectoryServerPickerViewController.m @@ -0,0 +1,303 @@ +/* + Copyright 2017 Vector Creations 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 "DirectoryServerPickerViewController.h" +#import "DirectoryServerTableViewCell.h" +#import "DirectoryServerDetailTableViewCell.h" + +#import "AppDelegate.h" + +@interface DirectoryServerPickerViewController () +{ + MXKDirectoryServersDataSource *dataSource; + + // Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. + id kAppDelegateDidTapStatusBarNotificationObserver; + + void (^onCompleteBlock)(id cellData); + + // Current alert (if any). + MXKAlert *currentAlert; + + // Current request in progress. + MXHTTPOperation *mxCurrentOperation; +} +@end + +@implementation DirectoryServerPickerViewController + +- (void)finalizeInit +{ + [super finalizeInit]; + + // Setup `MXKViewControllerHandling` properties + self.defaultBarTintColor = kRiotNavBarTintColor; + self.enableBarTintColorStatusChange = NO; + self.rageShakeManager = [RageShakeManager sharedManager]; +} + +- (void)destroy +{ + dataSource.delegate = nil; + dataSource = nil; + onCompleteBlock = nil; + + if (kAppDelegateDidTapStatusBarNotificationObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver]; + kAppDelegateDidTapStatusBarNotificationObserver = nil; + } + + // Close any pending actionsheet + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + if (mxCurrentOperation) + { + [mxCurrentOperation cancel]; + mxCurrentOperation = nil; + } + + [super destroy]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = NSLocalizedStringFromTable(@"directory_server_picker_title", @"Vector", nil); + + self.tableView.delegate = self; + + // Register view cell classes + [self.tableView registerClass:DirectoryServerTableViewCell.class forCellReuseIdentifier:DirectoryServerTableViewCell.defaultReuseIdentifier]; + [self.tableView registerClass:DirectoryServerDetailTableViewCell.class forCellReuseIdentifier:DirectoryServerDetailTableViewCell.defaultReuseIdentifier]; + + // Add a cancel button + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(onCancel:)]; + self.navigationItem.leftBarButtonItem.accessibilityIdentifier = @"DirectoryServerPickerVCCancelButton"; + + // Add a + button + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(onAdd:)]; + self.navigationItem.rightBarButtonItem.accessibilityIdentifier = @"DirectoryServerPickerVCAddButton"; + + // Hide line separators of empty cells + self.tableView.tableFooterView = [[UIView alloc] init]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Screen tracking (via Google Analytics) + id tracker = [[GAI sharedInstance] defaultTracker]; + if (tracker) + { + [tracker set:kGAIScreenName value:@"DirectoryServerPicker"]; + [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; + } + + // Observe kAppDelegateDidTapStatusBarNotificationObserver. + kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + + [self.tableView setContentOffset:CGPointMake(-self.tableView.contentInset.left, -self.tableView.contentInset.top) animated:YES]; + + }]; + + [dataSource loadData]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + if (kAppDelegateDidTapStatusBarNotificationObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver]; + kAppDelegateDidTapStatusBarNotificationObserver = nil; + } + + [super viewWillDisappear:animated]; +} + +- (void)displayWithDataSource:(MXKDirectoryServersDataSource*)theDataSource + onComplete:(void (^)(id cellData))onComplete; +{ + dataSource = theDataSource; + onCompleteBlock = onComplete; + + // Let the data source provide cells + self.tableView.dataSource = dataSource; + + dataSource.delegate = self; +} + +#pragma mark - MXKDataSourceDelegate + +- (Class)cellViewClassForCellData:(MXKCellData*)cellData +{ + id directoryCellData = (id)cellData; + + if (directoryCellData.homeserver) + { + return DirectoryServerDetailTableViewCell.class; + } + return DirectoryServerTableViewCell.class; +} + +- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData +{ + id directoryCellData = (id)cellData; + + if (directoryCellData.homeserver) + { + return DirectoryServerDetailTableViewCell.defaultReuseIdentifier; + } + return DirectoryServerTableViewCell.defaultReuseIdentifier; +} + +- (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id /* @TODO*/)changes +{ + [self.tableView reloadData]; +} + +- (void)dataSource:(MXKDataSource*)dataSource2 didStateChange:(MXKDataSourceState)state +{ + if (state == MXKDataSourceStatePreparing) + { + [self startActivityIndicator]; + } + else + { + [self stopActivityIndicator]; + [self.tableView reloadData]; + } +} + + +#pragma mark - UITableViewDelegate + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return DirectoryServerTableViewCell.cellHeight; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + id cellData = [dataSource cellDataAtIndexPath:indexPath]; + + if (onCompleteBlock) + { + onCompleteBlock(cellData); + } + + [self withdrawViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - User actions + +- (IBAction)onCancel:(id)sender +{ + if (onCompleteBlock) + { + onCompleteBlock(nil); + } + + [self withdrawViewControllerAnimated:YES completion:nil]; +} + +- (IBAction)onAdd:(id)sender +{ + __weak typeof(self) weakSelf = self; + + [currentAlert dismiss:NO]; + + // Prompt the user to enter a homeserver + currentAlert = [[MXKAlert alloc] initWithTitle:nil message:NSLocalizedStringFromTable(@"directory_server_type_homeserver", @"Vector", nil) style:MXKAlertStyleAlert]; + + [currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { + + textField.secureTextEntry = NO; + textField.placeholder = NSLocalizedStringFromTable(@"directory_server_placeholder", @"Vector", nil); + textField.keyboardType = UIKeyboardTypeDefault; + }]; + + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + } + + }]; + + [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + if (weakSelf) + { + UITextField *textField = [alert textFieldAtIndex:0]; + + typeof(self) self = weakSelf; + self->currentAlert = nil; + + NSString *homeserver = textField.text; + if (homeserver.length) + { + // Test if the homeserver exists + [self.activityIndicator startAnimating]; + + self->mxCurrentOperation = [self->dataSource.mxSession.matrixRestClient publicRoomsOnServer:homeserver limit:20 since:nil filter:nil thirdPartyInstanceId:nil includeAllNetworks:YES success:^(MXPublicRoomsResponse *publicRoomsResponse) { + + if (weakSelf && self->mxCurrentOperation) + { + // The homeserver is valid + self->mxCurrentOperation = nil; + [self.activityIndicator stopAnimating]; + + if (self->onCompleteBlock) + { + // Prepare response argument + MXKDirectoryServerCellData *cellData = [[MXKDirectoryServerCellData alloc] initWithHomeserver:homeserver includeAllNetworks:YES]; + + self->onCompleteBlock(cellData); + } + + [self withdrawViewControllerAnimated:YES completion:nil]; + } + + } failure:^(NSError *error) { + + if (weakSelf && self->mxCurrentOperation) + { + // The homeserver is not valid + self->mxCurrentOperation = nil; + [self.activityIndicator stopAnimating]; + + [[AppDelegate theDelegate] showErrorAsAlert:error]; + } + + }]; + } + } + }]; + + [currentAlert showInViewController:self]; +} + + +@end diff --git a/Riot/ViewController/DirectoryViewController.m b/Riot/ViewController/DirectoryViewController.m index 9ce2f22ad..8016bdbb4 100644 --- a/Riot/ViewController/DirectoryViewController.m +++ b/Riot/ViewController/DirectoryViewController.m @@ -21,10 +21,6 @@ #import "AppDelegate.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - @interface DirectoryViewController () { PublicRoomsDirectoryDataSource *dataSource; @@ -88,10 +84,10 @@ { [super viewDidAppear:animated]; - // Release the current selected room (if any) except if the Room ViewController is still visible (see splitViewController.isCollapsed condition) + // Release the current selected item (room/contact...) except if the second view controller is still visible (see splitViewController.isCollapsed condition) if (self.splitViewController && self.splitViewController.isCollapsed) { - [[AppDelegate theDelegate].homeViewController closeSelectedRoom]; + [[AppDelegate theDelegate].masterTabBarController releaseSelectedItem]; } else { @@ -150,13 +146,13 @@ [self stopActivityIndicator]; - [[AppDelegate theDelegate].homeViewController showRoomPreview:roomPreviewData]; + [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; }]; } else { RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; - [[AppDelegate theDelegate].homeViewController showRoomPreview:roomPreviewData]; + [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; } } @@ -175,19 +171,19 @@ - (void)openRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)mxSession { - [[AppDelegate theDelegate].homeViewController selectRoomWithId:roomId andEventId:nil inMatrixSession:mxSession]; + [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:roomId andEventId:nil inMatrixSession:mxSession]; } - (void)refreshCurrentSelectedCell:(BOOL)forceVisible { - HomeViewController *homeViewController = [AppDelegate theDelegate].homeViewController; + MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. NSIndexPath *currentSelectedCellIndexPath = nil; - if (homeViewController.currentRoomViewController) + if (masterTabBarController.currentRoomViewController) { // Look for the rank of this selected room in displayed recents - currentSelectedCellIndexPath = [dataSource cellIndexPathWithRoomId:homeViewController.selectedRoomId andMatrixSession:homeViewController.selectedRoomSession]; + currentSelectedCellIndexPath = [dataSource cellIndexPathWithRoomId:masterTabBarController.selectedRoomId andMatrixSession:masterTabBarController.selectedRoomSession]; } if (currentSelectedCellIndexPath) @@ -224,30 +220,42 @@ [self addSpinnerFooterView]; + __weak __typeof(self) weakSelf = self; + [dataSource paginate:^(NSUInteger roomsAdded) { - if (roomsAdded) + if (weakSelf) { - // Notify the table view there are new items at its tail - NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:roomsAdded]; + __strong __typeof(weakSelf) self = weakSelf; - NSUInteger numberOfRowsBefore = [dataSource tableView:self.tableView numberOfRowsInSection:0]; - numberOfRowsBefore -= roomsAdded; - - for (NSUInteger i = 0; i < roomsAdded; i++) + if (roomsAdded) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(numberOfRowsBefore + i) inSection:0]; - [indexPaths addObject:indexPath]; + // Notify the table view there are new items at its tail + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:roomsAdded]; + + NSUInteger numberOfRowsBefore = [self->dataSource tableView:self.tableView numberOfRowsInSection:0]; + numberOfRowsBefore -= roomsAdded; + + for (NSUInteger i = 0; i < roomsAdded; i++) + { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(numberOfRowsBefore + i) inSection:0]; + [indexPaths addObject:indexPath]; + } + + [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic]; } - - [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic]; + + [self removeSpinnerFooterView]; } - [self removeSpinnerFooterView]; - } failure:^(NSError *error) { - [self removeSpinnerFooterView]; + if (weakSelf) + { + __strong __typeof(weakSelf) self = weakSelf; + + [self removeSpinnerFooterView]; + } }]; } diff --git a/Riot/ViewController/FavouritesViewController.h b/Riot/ViewController/FavouritesViewController.h new file mode 100644 index 000000000..3cc75364f --- /dev/null +++ b/Riot/ViewController/FavouritesViewController.h @@ -0,0 +1,29 @@ +/* + Copyright 2017 Vector Creations 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" + +/** + The `FavouritesViewController` screen is the view controller displayed when `Favourites` tab is selected. + */ +@interface FavouritesViewController : RecentsViewController + +/** + Scroll the next room with missed notifications to the top. + */ +- (void)scrollToNextRoomWithMissedNotifications; + +@end diff --git a/Riot/ViewController/FavouritesViewController.m b/Riot/ViewController/FavouritesViewController.m new file mode 100644 index 000000000..a5352cb8a --- /dev/null +++ b/Riot/ViewController/FavouritesViewController.m @@ -0,0 +1,136 @@ +/* + Copyright 2017 Vector Creations 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 "FavouritesViewController.h" + +#import "AppDelegate.h" + +#import "RecentsDataSource.h" + +@interface FavouritesViewController () +{ + RecentsDataSource *recentsDataSource; +} + +@end + +@implementation FavouritesViewController + +- (void)finalizeInit +{ + [super finalizeInit]; + + self.screenName = @"Favourites"; + + self.enableDragging = YES; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.accessibilityIdentifier = @"FavouritesVCView"; + self.recentsTableView.accessibilityIdentifier = @"FavouritesVCTableView"; + + // Tag the recents table with the its recents data source mode. + // This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods). + self.recentsTableView.tag = RecentsDataSourceModeFavourites; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_favourites", @"Vector", nil); + [AppDelegate theDelegate].masterTabBarController.navigationController.navigationBar.tintColor = kRiotColorIndigo; + [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = kRiotColorIndigo; + + if (recentsDataSource) + { + // Take the lead on the shared data source. + recentsDataSource.areSectionsShrinkable = NO; + [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeFavourites]; + } +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + if ([AppDelegate theDelegate].masterTabBarController.tabBar.tintColor == kRiotColorIndigo) + { + // Restore default tintColor + [AppDelegate theDelegate].masterTabBarController.navigationController.navigationBar.tintColor = kRiotColorGreen; + [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = kRiotColorGreen; + } +} + +- (void)dealloc +{ + +} + +- (void)destroy +{ + [super destroy]; +} + +#pragma mark - + +- (void)displayList:(MXKRecentsDataSource *)listDataSource +{ + [super displayList:listDataSource]; + + // Keep a ref on the recents data source + if ([listDataSource isKindOfClass:RecentsDataSource.class]) + { + recentsDataSource = (RecentsDataSource*)listDataSource; + } +} + +#pragma mark - Override RecentsViewController + +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible +{ + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode != RecentsDataSourceModeFavourites) + { + return; + } + + [super refreshCurrentSelectedCell:forceVisible]; +} + +#pragma mark - + +- (void)scrollToNextRoomWithMissedNotifications +{ + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode == RecentsDataSourceModeFavourites) + { + [self scrollToTheTopTheNextRoomWithMissedNotificationsInSection:recentsDataSource.favoritesSection]; + } +} + +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + // Hide the unique header + return 0.0f; +} + +@end diff --git a/Riot/ViewController/HomeFilesSearchViewController.m b/Riot/ViewController/HomeFilesSearchViewController.m index aec338e01..7d08185b9 100644 --- a/Riot/ViewController/HomeFilesSearchViewController.m +++ b/Riot/ViewController/HomeFilesSearchViewController.m @@ -26,10 +26,6 @@ #import "EventFormatter.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - @implementation HomeFilesSearchViewController - (void)finalizeInit @@ -118,8 +114,8 @@ [tableView deselectRowAtIndexPath:indexPath animated:YES]; - // Make the HomeViewController (that contains this VC) open the RoomViewController - [self.parentViewController performSegueWithIdentifier:@"showDetails" sender:self]; + // Make the master tabBar view controller open the RoomViewController + [[AppDelegate theDelegate].masterTabBarController performSegueWithIdentifier:@"showRoomDetails" sender:self]; // Reset the selected event. HomeViewController got it when here _selectedEvent = nil; diff --git a/Riot/ViewController/HomeMessagesSearchViewController.m b/Riot/ViewController/HomeMessagesSearchViewController.m index 4fbd14a66..2f6049091 100644 --- a/Riot/ViewController/HomeMessagesSearchViewController.m +++ b/Riot/ViewController/HomeMessagesSearchViewController.m @@ -30,10 +30,6 @@ #import "EventFormatter.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - @implementation HomeMessagesSearchViewController - (void)finalizeInit @@ -170,8 +166,8 @@ [tableView deselectRowAtIndexPath:indexPath animated:YES]; - // Make the HomeViewController (that contains this VC) open the RoomViewController - [self.parentViewController performSegueWithIdentifier:@"showDetails" sender:self]; + // Make the master tabBar view controller open the RoomViewController + [[AppDelegate theDelegate].masterTabBarController performSegueWithIdentifier:@"showRoomDetails" sender:self]; // Reset the selected event. HomeViewController got it when here _selectedEvent = nil; diff --git a/Riot/ViewController/HomeViewController.h b/Riot/ViewController/HomeViewController.h index 0416cd697..a397c2598 100644 --- a/Riot/ViewController/HomeViewController.h +++ b/Riot/ViewController/HomeViewController.h @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket Ltd + Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,80 +15,11 @@ limitations under the License. */ -#import - -#import "SegmentedViewController.h" - -#import "ContactsTableViewController.h" - -#import "RoomViewController.h" -#import "AuthenticationViewController.h" +#import "RecentsViewController.h" /** The `HomeViewController` screen is the main app screen. */ -@interface HomeViewController : SegmentedViewController - -@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsBarButtonItem; -@property (weak, nonatomic) IBOutlet UIBarButtonItem *searchBarButtonIem; - -// References on the currently selected room and its view controller -@property (nonatomic, readonly) RoomViewController *currentRoomViewController; -@property (nonatomic, readonly) NSString *selectedRoomId; -@property (nonatomic, readonly) NSString *selectedEventId; -@property (nonatomic, readonly) MXSession *selectedRoomSession; -@property (nonatomic, readonly) RoomPreviewData *selectedRoomPreviewData; - -// Reference to the current auth VC. It is not nil only when the auth screen is displayed. -@property (nonatomic, readonly) AuthenticationViewController *authViewController; - -/** - Display the authentication screen. - */ -- (void)showAuthenticationScreen; - -/** - Display the authentication screen in order to pursue a registration process by using a predefined set - of parameters. - - If the provided registration parameters are not supported, we switch back to the default login screen. - - @param parameters the set of parameters. - */ -- (void)showAuthenticationScreenWithRegistrationParameters:(NSDictionary*)parameters; - -/** - Open the room with the provided identifier in a specific matrix session. - - @param roomId the room identifier. - @param eventId if not nil, the room will be opened on this event. - @param mxSession the matrix session in which the room should be available. - */ -- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)mxSession; - -/** - Open the RoomViewController to display the preview of a room that is unknown for the user. - - This room can come from an email invitation link or a simple link to a room. - - @param roomPreviewData the data for the room preview. - */ -- (void)showRoomPreview:(RoomPreviewData*)roomPreviewData; - -/** - Close the current selected room (if any) - */ -- (void)closeSelectedRoom; - -/** - Open the public rooms directory page. - It uses the `publicRoomsDirectoryDataSource` managed by the recents view controller data source - */ -- (void)showPublicRoomsDirectory; - -/** - Action registered on `UIControlEventTouchUpInside` event for both buttons. - */ -- (IBAction)onButtonPressed:(id)sender; +@interface HomeViewController : RecentsViewController @end diff --git a/Riot/ViewController/HomeViewController.m b/Riot/ViewController/HomeViewController.m index 7c45e6d2b..1bb6685bd 100644 --- a/Riot/ViewController/HomeViewController.m +++ b/Riot/ViewController/HomeViewController.m @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket Ltd + Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,62 +17,17 @@ #import "HomeViewController.h" -#import "RecentsDataSource.h" -#import "RecentsViewController.h" - -#import "RoomDataSource.h" -#import "RoomViewController.h" - -#import "DirectoryViewController.h" -#import "ContactDetailsViewController.h" -#import "SettingsViewController.h" - -#import "HomeMessagesSearchViewController.h" -#import "HomeMessagesSearchDataSource.h" -#import "HomeFilesSearchViewController.h" -#import "FilesSearchCellData.h" - #import "AppDelegate.h" -#import "GBDeviceInfo_iOS.h" +#import "RecentsDataSource.h" + +#import "TableViewCellWithCollectionView.h" +#import "RoomCollectionViewCell.h" @interface HomeViewController () { - RecentsViewController *recentsViewController; RecentsDataSource *recentsDataSource; - - HomeMessagesSearchViewController *messagesSearchViewController; - HomeMessagesSearchDataSource *messagesSearchDataSource; - - HomeFilesSearchViewController *filesSearchViewController; - MXKSearchDataSource *filesSearchDataSource; - - ContactsTableViewController *peopleSearchViewController; - MXKContact *selectedContact; - - // Display a gradient view above the screen - CAGradientLayer* tableViewMaskLayer; - - // Display a button to a new room - UIImageView* createNewRoomImageView; - - MXHTTPOperation *roomCreationRequest; - - // Tell whether the authentication screen is preparing. - BOOL isAuthViewControllerPreparing; - - // Observer that checks when the Authentification view controller has gone. - id authViewControllerObserver; - - // The parameters to pass to the Authentification view controller. - NSDictionary *authViewControllerRegistrationParameters; - - // Current alert (if any). - MXKAlert *currentAlert; } - -@property(nonatomic,getter=isHidden) BOOL hidden; - @end @implementation HomeViewController @@ -80,1114 +36,226 @@ { [super finalizeInit]; - // The navigation bar tint color and the rageShake Manager are handled by super (see SegmentedViewController). + self.screenName = @"Home"; } - (void)viewDidLoad { - // Set up the SegmentedVC tabs before calling [super viewDidLoad] - NSMutableArray* viewControllers = [[NSMutableArray alloc] init]; - NSMutableArray* titles = [[NSMutableArray alloc] init]; - - [titles addObject: NSLocalizedStringFromTable(@"search_rooms", @"Vector", nil)]; - recentsViewController = [RecentsViewController recentListViewController]; - recentsViewController.delegate = self; - [viewControllers addObject:recentsViewController]; - - [titles addObject: NSLocalizedStringFromTable(@"search_messages", @"Vector", nil)]; - messagesSearchViewController = [HomeMessagesSearchViewController searchViewController]; - [viewControllers addObject:messagesSearchViewController]; - - // Add search People tab - [titles addObject: NSLocalizedStringFromTable(@"search_people", @"Vector", nil)]; - peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; - peopleSearchViewController.contactsTableViewControllerDelegate = self; - peopleSearchViewController.contactCellAccessoryType = UITableViewCellAccessoryDisclosureIndicator; - [viewControllers addObject:peopleSearchViewController]; - - // add Files tab - [titles addObject: NSLocalizedStringFromTable(@"search_files", @"Vector", nil)]; - filesSearchViewController = [HomeFilesSearchViewController searchViewController]; - [viewControllers addObject:filesSearchViewController]; - - [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; - [super viewDidLoad]; - self.navigationItem.title = NSLocalizedStringFromTable(@"title_recents", @"Vector", nil); - - // Add the Vector background image when search bar is empty - [self addBackgroundImageViewToView:self.view]; + self.view.accessibilityIdentifier = @"HomeVCView"; + self.recentsTableView.accessibilityIdentifier = @"HomeVCTableView"; - // Add room creation button programatically - [self addRoomCreationButton]; + // Tag the recents table with the its recents data source mode. + // This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods). + self.recentsTableView.tag = RecentsDataSourceModeHome; - // Initialize here the data sources if a matrix session has been already set. - [self initializeDataSources]; + // Add the (+) button programmatically + [self addPlusButton]; - self.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone; - self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); -} - -- (void)dealloc -{ - [self closeSelectedRoom]; -} - -- (void)destroy -{ - [super destroy]; - - if (currentAlert) - { - [currentAlert dismiss:NO]; - currentAlert = nil; - } - - if (authViewControllerObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:authViewControllerObserver]; - authViewControllerObserver = nil; - } - - if (roomCreationRequest) - { - [roomCreationRequest cancel]; - roomCreationRequest = nil; - } + // Register table view cell used for rooms collection. + [self.recentsTableView registerClass:TableViewCellWithCollectionView.class forCellReuseIdentifier:TableViewCellWithCollectionView.defaultReuseIdentifier]; - if (createNewRoomImageView) - { - [createNewRoomImageView removeFromSuperview]; - createNewRoomImageView = nil; - tableViewMaskLayer = nil; - } + // Change the table data source. It must be the home view controller itself. + self.recentsTableView.dataSource = self; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Show the home view controller content only when a user is logged in. - self.hidden = ([MXKAccountManager sharedManager].accounts.count == 0); - - // Let's child display the loading not the home view controller - if (self.activityIndicator) - { - [self.activityIndicator stopAnimating]; - self.activityIndicator = nil; - } + [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_home", @"Vector", nil); - // Refresh the search results if a search in in progress - if (!self.searchBarHidden) + if (recentsDataSource) { - [self updateSearch]; + // Take the lead on the shared data source. + recentsDataSource.areSectionsShrinkable = NO; + [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeHome]; } + + [self moveAllCollectionsToLeft]; } -- (void)viewDidAppear:(BOOL)animated +- (void)dealloc { - [super viewDidAppear:animated]; - // Check whether we're not logged in - if (![MXKAccountManager sharedManager].accounts.count) - { - [self showAuthenticationScreen]; - } - else - { - // Check whether the user has been already prompted to send crash reports. - // (Check whether 'enableCrashReport' flag has been set once) - if (![[NSUserDefaults standardUserDefaults] objectForKey:@"enableCrashReport"]) - { - [self promptUserBeforeUsingGoogleAnalytics]; - } - - // Release the current selected room (if any) except if the Room ViewController is still visible (see splitViewController.isCollapsed condition) - if (!self.splitViewController || self.splitViewController.isCollapsed) - { - // Release the current selected room (if any). - [self closeSelectedRoom]; - } - else - { - // In case of split view controller where the primary and secondary view controllers are displayed side-by-side onscreen, - // the selected room (if any) is highlighted. - [self refreshCurrentSelectedCellInChild:YES]; - } - } - - // Here the actual view size is available, check the background image display if any - if (!self.searchBarHidden) - { - [self checkAndShowBackgroundImage]; - } } -- (void)viewDidLayoutSubviews +- (void)destroy { - [super viewDidLayoutSubviews]; + [super destroy]; +} - // sanity check - if (tableViewMaskLayer) +- (void)moveAllCollectionsToLeft +{ + // Scroll all rooms collections to their beginning + for (NSInteger section = 0; section < [self numberOfSectionsInTableView:self.recentsTableView]; section++) { - CGRect currentBounds = tableViewMaskLayer.bounds; - CGRect newBounds = CGRectIntegral(self.view.frame); - - // check if there is an update - if (!CGSizeEqualToSize(currentBounds.size, newBounds.size)) + UITableViewCell *firstSectionCell = [self.recentsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; + if (firstSectionCell && [firstSectionCell isKindOfClass:TableViewCellWithCollectionView.class]) { - newBounds.origin = CGPointZero; - tableViewMaskLayer.bounds = newBounds; - } - } -} + TableViewCellWithCollectionView *tableViewCell = (TableViewCellWithCollectionView*)firstSectionCell; -#pragma mark - - -- (void)showAuthenticationScreen -{ - // Check whether an authentication screen is not already shown or preparing - if (!self.authViewController && !isAuthViewControllerPreparing) - { - isAuthViewControllerPreparing = YES; - - [[AppDelegate theDelegate] restoreInitialDisplay:^{ - - [self performSegueWithIdentifier:@"showAuth" sender:self]; - - }]; - } -} - -- (void)showAuthenticationScreenWithRegistrationParameters:(NSDictionary *)parameters -{ - if (self.authViewController) - { - NSLog(@"[HomeViewController] Universal link: Forward registration parameter to the existing AuthViewController"); - self.authViewController.externalRegistrationParameters = parameters; - } - else - { - NSLog(@"[HomeViewController] Universal link: Logout current sessions and open AuthViewController to complete the registration"); - - // Keep a ref on the params - authViewControllerRegistrationParameters = parameters; - - // And do a logout out. It will then display AuthViewController - [[AppDelegate theDelegate] logout]; - } -} - -- (void)initializeDataSources -{ - MXSession *mainSession = self.mainSession; - - if (mainSession) - { - // Init the recents data source - recentsDataSource = [[RecentsDataSource alloc] initWithMatrixSession:mainSession]; - [recentsViewController displayList:recentsDataSource fromHomeViewController:self]; - - // Init the search for messages - messagesSearchDataSource = [[HomeMessagesSearchDataSource alloc] initWithMatrixSession:mainSession]; - [messagesSearchViewController displaySearch:messagesSearchDataSource]; - - // Init the search for messages - filesSearchDataSource = [[MXKSearchDataSource alloc] initWithMatrixSession:mainSession]; - filesSearchDataSource.roomEventFilter.containsURL = YES; - filesSearchDataSource.shouldShowRoomDisplayName = YES; - [filesSearchDataSource registerCellDataClass:FilesSearchCellData.class forCellIdentifier:kMXKSearchCellDataIdentifier]; - [filesSearchViewController displaySearch:filesSearchDataSource]; - - // Check whether there are others sessions - NSArray* mxSessions = self.mxSessions; - if (mxSessions.count > 1) - { - for (MXSession *mxSession in mxSessions) + if ([tableViewCell.collectionView numberOfItemsInSection:0] > 0) { - if (mxSession != mainSession) - { - // Add the session to the recents data source - [recentsDataSource addMatrixSession:mxSession]; - - // FIXME: Update messagesSearchDataSource and filesSearchDataSource - } + [tableViewCell.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:NO]; } } - - // Do not go to search mode when first opening the home - [self hideSearch:NO]; - - // Do the one time check on device id - [self checkDeviceId]; } } -- (void)addMatrixSession:(MXSession *)mxSession +#pragma mark - Override RecentsViewController + +- (void)displayList:(MXKRecentsDataSource *)listDataSource { - // Check whether the controller'€™s view is loaded into memory. - if (recentsViewController) - { - // Check whether the data sources have been initialized. - if (!recentsDataSource) - { - // Add first the session. The updated sessions list will be used during data sources initialization. - [super addMatrixSession:mxSession]; - - // Prepare data sources and return - [self initializeDataSources]; - return; - } - else - { - // Add the session to the existing recents data source - [recentsDataSource addMatrixSession:mxSession]; - - // FIXME: Update messagesSearchDataSource and filesSearchDataSource - } - } + [super displayList:listDataSource]; - [super addMatrixSession:mxSession]; + // Change the table data source. It must be the home view controller itself. + self.recentsTableView.dataSource = self; + + // Keep a ref on the recents data source + if ([listDataSource isKindOfClass:RecentsDataSource.class]) + { + recentsDataSource = (RecentsDataSource*)listDataSource; + } } -- (void)removeMatrixSession:(MXSession *)mxSession +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible { - [recentsDataSource removeMatrixSession:mxSession]; - - // Check whether there are others sessions - if (!recentsDataSource.mxSessions.count) + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode != RecentsDataSourceModeHome) { - [recentsViewController displayList:nil]; - [recentsDataSource destroy]; - recentsDataSource = nil; - } - - // FIXME: Handle correctly messagesSearchDataSource and filesSearchDataSource - - [super removeMatrixSession:mxSession]; -} - -- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)matrixSession -{ - // Force hiding the keyboard - [self.searchBar resignFirstResponder]; - - if (_selectedRoomId && [_selectedRoomId isEqualToString:roomId] - && _selectedEventId && [_selectedEventId isEqualToString:eventId] - && _selectedRoomSession && _selectedRoomSession == matrixSession) - { - // Nothing to do return; } - - _selectedRoomId = roomId; - _selectedEventId = eventId; - _selectedRoomSession = matrixSession; - - if (roomId && matrixSession) - { - [self performSegueWithIdentifier:@"showDetails" sender:self]; - } - else - { - [self closeSelectedRoom]; - } -} - -- (void)showRoomPreview:(RoomPreviewData *)roomPreviewData -{ - // Force hiding the keyboard - [self.searchBar resignFirstResponder]; - - _selectedRoomPreviewData = roomPreviewData; - _selectedRoomId = roomPreviewData.roomId; - _selectedRoomSession = roomPreviewData.mxSession; - - [self performSegueWithIdentifier:@"showDetails" sender:self]; -} - -- (void)closeSelectedRoom -{ - _selectedRoomId = nil; - _selectedEventId = nil; - _selectedRoomSession = nil; - - if (_currentRoomViewController) - { - // If the displayed data is not a preview, let the manager release the room data source - // (except if the view controller has the room data source ownership). - if (!_currentRoomViewController.roomPreviewData && _currentRoomViewController.roomDataSource && !_currentRoomViewController.hasRoomDataSourceOwnership) - { - MXSession *mxSession = _currentRoomViewController.roomDataSource.mxSession; - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession]; - - // Let the manager release live room data sources where the user is in - [roomDataSourceManager closeRoomDataSource:_currentRoomViewController.roomDataSource forceClose:NO]; - } - - [_currentRoomViewController destroy]; - _currentRoomViewController = nil; - } -} - -- (void)showPublicRoomsDirectory -{ - // Force hiding the keyboard - [self.searchBar resignFirstResponder]; - [self performSegueWithIdentifier:@"showDirectory" sender:self]; + // TODO: refreshCurrentSelectedCell + //[super refreshCurrentSelectedCell:forceVisible]; } -#pragma mark - Override MXKViewController - -- (void)setKeyboardHeight:(CGFloat)keyboardHeight +- (void)didTapOnSectionHeader:(UIGestureRecognizer*)gestureRecognizer { - [self setKeyboardHeightForBackgroundImage:keyboardHeight]; - - [super setKeyboardHeight:keyboardHeight]; + UIView *view = gestureRecognizer.view; + NSInteger section = view.tag; - [self checkAndShowBackgroundImage]; -} - -- (void)startActivityIndicator -{ - // Redirect the operation to the currently displayed VC - // It is a MXKViewController or a MXKTableViewController. So it supports startActivityIndicator - [self.selectedViewController performSelector:@selector(startActivityIndicator)]; -} - -- (void)stopActivityIndicator -{ - // The selected view controller mwy have changed since the call of [self startActivityIndicator] - // So, stop the activity indicator for all children - for (UIViewController *viewController in self.viewControllers) + // Scroll to the top this section + if ([self.recentsTableView numberOfRowsInSection:section] > 0) { - [viewController performSelector:@selector(stopActivityIndicator)]; - } - } - -#pragma mark - Override UIViewController+VectorSearch - -- (void)setKeyboardHeightForBackgroundImage:(CGFloat)keyboardHeight -{ - [super setKeyboardHeightForBackgroundImage:keyboardHeight]; - - if (keyboardHeight > 0) - { - [self checkAndShowBackgroundImage]; - } -} - -// Check conditions before displaying the background -- (void)checkAndShowBackgroundImage -{ - // Note: This background is hidden when keyboard is dismissed. - // The other conditions depend on the current selected view controller. - if (self.selectedViewController == recentsViewController) - { - self.backgroundImageView.hidden = (!recentsDataSource.hideRecents || !recentsDataSource.hidePublicRoomsDirectory || (self.keyboardHeight == 0)); - } - else if (self.selectedViewController == messagesSearchViewController) - { - self.backgroundImageView.hidden = ((messagesSearchDataSource.serverCount != 0) || !messagesSearchViewController.noResultsLabel.isHidden || (self.keyboardHeight == 0)); - } - else if (self.selectedViewController == peopleSearchViewController) - { - self.backgroundImageView.hidden = (([peopleSearchViewController.tableView numberOfSections] != 0) || (self.keyboardHeight == 0)); - } - else if (self.selectedViewController == filesSearchViewController) - { - self.backgroundImageView.hidden = ((filesSearchDataSource.serverCount != 0) || !filesSearchViewController.noResultsLabel.isHidden || (self.keyboardHeight == 0)); - } - else - { - self.backgroundImageView.hidden = (self.keyboardHeight == 0); + [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:YES]; } - if (!self.backgroundImageView.hidden) + // Scroll to the beginning the corresponding rooms collection. + UITableViewCell *firstSectionCell = [self.recentsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; + if (firstSectionCell && [firstSectionCell isKindOfClass:TableViewCellWithCollectionView.class]) { - [self.backgroundImageView layoutIfNeeded]; - [self.selectedViewController.view layoutIfNeeded]; + TableViewCellWithCollectionView *tableViewCell = (TableViewCellWithCollectionView*)firstSectionCell; - // Check whether there is enough space to display this background - // For example, in landscape with the iPhone 5 & 6 screen size, the backgroundImageView must be hidden. - if (self.backgroundImageView.frame.origin.y < 0 || (self.selectedViewController.view.frame.size.height - self.backgroundImageViewBottomConstraint.constant) < self.backgroundImageView.frame.size.height) + if ([tableViewCell.collectionView numberOfItemsInSection:0] > 0) { - self.backgroundImageView.hidden = YES; + [tableViewCell.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES]; } } } -#pragma mark - Override SegmentedViewController +#pragma mark - UITableViewDataSource -- (void)setSelectedIndex:(NSUInteger)selectedIndex +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - [super setSelectedIndex:selectedIndex]; + // Return the actual number of sections prepared in recents dataSource. + return [recentsDataSource numberOfSectionsInTableView:tableView]; +} - if (!self.searchBarHidden) +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + // Each rooms section is represented by only one collection view. + return 1; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if ((indexPath.section == recentsDataSource.conversationSection && !recentsDataSource.conversationCellDataArray.count) + || (indexPath.section == recentsDataSource.peopleSection && !recentsDataSource.peopleCellDataArray.count)) { - if (self.selectedViewController == peopleSearchViewController) - { - self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_people_placeholder", @"Vector", nil); - } - else - { - self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); - } + return [recentsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; + } + + TableViewCellWithCollectionView *tableViewCell = [tableView dequeueReusableCellWithIdentifier:TableViewCellWithCollectionView.defaultReuseIdentifier forIndexPath:indexPath]; + tableViewCell.collectionView.tag = indexPath.section; + [tableViewCell.collectionView registerClass:RoomCollectionViewCell.class forCellWithReuseIdentifier:RoomCollectionViewCell.defaultReuseIdentifier]; + tableViewCell.collectionView.delegate = self; + tableViewCell.collectionView.dataSource = self; + tableViewCell.selectionStyle = UITableViewCellSelectionStyleNone; + + return tableViewCell; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return NO; +} + +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if ((indexPath.section == recentsDataSource.conversationSection && !recentsDataSource.conversationCellDataArray.count) + || (indexPath.section == recentsDataSource.peopleSection && !recentsDataSource.peopleCellDataArray.count)) + { + return [recentsDataSource cellHeightAtIndexPath:indexPath]; + } + + // Return the fixed height of the collection view cell used to display a room. + return [RoomCollectionViewCell defaultCellSize].height; +} + +#pragma mark - UICollectionViewDataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return [recentsDataSource tableView:self.recentsTableView numberOfRowsInSection:collectionView.tag]; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + RoomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:RoomCollectionViewCell.defaultReuseIdentifier + forIndexPath:indexPath]; + + id cellData = [recentsDataSource cellDataAtIndexPath:[NSIndexPath indexPathForRow:indexPath.item inSection:collectionView.tag]]; + + if (cellData) + { + [cell render:cellData]; + cell.tag = indexPath.item; - [self updateSearch]; + //TODO: add long tap gesture recognizer. +// UILongPressGestureRecognizer *cellLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellLongPress:)]; +// [cell addGestureRecognizer:cellLongPressGesture]; } + + return cell; } -#pragma mark - Internal methods +#pragma mark - UICollectionViewDelegate -- (void)addRoomCreationButton +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - // Add blur mask programatically - tableViewMaskLayer = [CAGradientLayer layer]; - - CGColorRef opaqueWhiteColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor; - CGColorRef transparentWhiteColor = [UIColor colorWithWhite:1.0 alpha:0].CGColor; - - tableViewMaskLayer.colors = [NSArray arrayWithObjects:(__bridge id)transparentWhiteColor, (__bridge id)transparentWhiteColor, (__bridge id)opaqueWhiteColor, nil]; - - // display a gradient to the rencents bottom (20% of the bottom of the screen) - tableViewMaskLayer.locations = [NSArray arrayWithObjects: - [NSNumber numberWithFloat:0], - [NSNumber numberWithFloat:0.85], - [NSNumber numberWithFloat:1.0], nil]; - - tableViewMaskLayer.bounds = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); - tableViewMaskLayer.anchorPoint = CGPointZero; - - // CAConstraint is not supported on IOS. - // it seems only being supported on Mac OS. - // so viewDidLayoutSubviews will refresh the layout bounds. - [self.view.layer addSublayer:tableViewMaskLayer]; - - // Add room create button - createNewRoomImageView = [[UIImageView alloc] init]; - [createNewRoomImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.view addSubview:createNewRoomImageView]; - - createNewRoomImageView.backgroundColor = [UIColor clearColor]; - createNewRoomImageView.contentMode = UIViewContentModeCenter; - createNewRoomImageView.image = [UIImage imageNamed:@"create_room"]; - - CGFloat side = 78.0f; - NSLayoutConstraint* widthConstraint = [NSLayoutConstraint constraintWithItem:createNewRoomImageView - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:side]; - - NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:createNewRoomImageView - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1 - constant:side]; - - NSLayoutConstraint* centerXConstraint = [NSLayoutConstraint constraintWithItem:createNewRoomImageView - attribute:NSLayoutAttributeCenterX - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeCenterX - multiplier:1 - constant:0]; - - NSLayoutConstraint* bottomConstraint = [NSLayoutConstraint constraintWithItem:self.view - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:createNewRoomImageView - attribute:NSLayoutAttributeBottom - multiplier:1 - constant:9]; - - // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, centerXConstraint, bottomConstraint]]; - - createNewRoomImageView.userInteractionEnabled = YES; - - // Handle tap gesture - UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onNewRoomPressed)]; - [tap setNumberOfTouchesRequired:1]; - [tap setNumberOfTapsRequired:1]; - [tap setDelegate:self]; - [createNewRoomImageView addGestureRecognizer:tap]; -} - -- (void)promptUserBeforeUsingGoogleAnalytics -{ - NSLog(@"[HomeViewController]: Invite the user to send crash reports"); - - __weak typeof(self) weakSelf = self; - - [currentAlert dismiss:NO]; - - NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]; - - currentAlert = [[MXKAlert alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedStringFromTable(@"google_analytics_use_prompt", @"Vector", nil), appDisplayName] - message:nil - style:MXKAlertStyleAlert]; - - currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"] - style:MXKAlertActionStyleDefault - handler:^(MXKAlert *alert) { - - [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"enableCrashReport"]; - [[NSUserDefaults standardUserDefaults] synchronize]; - - if (weakSelf) - { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - } - - }]; - [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"] - style:MXKAlertActionStyleDefault - handler:^(MXKAlert *alert) { - - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"enableCrashReport"]; - [[NSUserDefaults standardUserDefaults] synchronize]; - - if (weakSelf) - { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - } - - [[AppDelegate theDelegate] startGoogleAnalytics]; - - }]; - - currentAlert.mxkAccessibilityIdentifier = @"HomeVCUseGoogleAnalyticsAlert"; - [currentAlert showInViewController:self]; -} - -// Made the currently displayed child update its selected cell -- (void)refreshCurrentSelectedCellInChild:(BOOL)forceVisible -{ - // TODO: Manage other children than recents - [recentsViewController refreshCurrentSelectedCell:forceVisible]; -} - -- (void)setHidden:(BOOL)hidden -{ - _hidden = hidden; - - self.selectionContainer.hidden = hidden; - self.viewControllerContainer.hidden = hidden; - self.navigationController.navigationBar.hidden = hidden; - - createNewRoomImageView.hidden = (hidden ? YES : !self.searchBarHidden); -} - -/** - Check the existence of device id. - */ -- (void)checkDeviceId -{ - // In case of the app update for the e2e encryption, the app starts with - // no device id provided by the homeserver. - // Ask the user to login again in order to enable e2e. Ask it once - if (![[NSUserDefaults standardUserDefaults] boolForKey:@"deviceIdAtStartupChecked"]) + if (self.delegate) { - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"deviceIdAtStartupChecked"]; - [[NSUserDefaults standardUserDefaults] synchronize]; - - // Check if there is a device id - if (!self.mainSession.matrixRestClient.credentials.deviceId) - { - NSLog(@"WARNING: The user has no device. Prompt for login again"); - - NSString *msg = NSLocalizedStringFromTable(@"e2e_enabling_on_app_update", @"Vector", nil); - - __weak typeof(self) weakSelf = self; - [currentAlert dismiss:NO]; - currentAlert = [[MXKAlert alloc] initWithTitle:nil message:msg style:MXKAlertStyleAlert]; - - currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"later"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - - if (weakSelf) - { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - } - - }]; - - [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - - if (weakSelf) - { - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - - [strongSelf startActivityIndicator]; - - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - - [[MXKAccountManager sharedManager] logout]; - - }); - } - - }]; - - currentAlert.mxkAccessibilityIdentifier = @"HomeVCCheckDeviceIdAlert"; - [currentAlert showInViewController:self]; - } - } -} - -#pragma mark - Navigation - -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender -{ - if ([[segue identifier] isEqualToString:@"showDetails"]) - { - UIViewController *controller; - if ([[segue destinationViewController] isKindOfClass:[UINavigationController class]]) - { - controller = [[segue destinationViewController] topViewController]; - } - else - { - controller = [segue destinationViewController]; - } - - if ([controller isKindOfClass:[RoomViewController class]]) - { - // Release existing Room view controller (if any) - if (_currentRoomViewController) - { - // If the displayed data is not a preview, let the manager release the room data source - // (except if the view controller has the room data source ownership). - if (!_currentRoomViewController.roomPreviewData && _currentRoomViewController.roomDataSource && !_currentRoomViewController.hasRoomDataSourceOwnership) - { - MXSession *mxSession = _currentRoomViewController.roomDataSource.mxSession; - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession]; - - [roomDataSourceManager closeRoomDataSource:_currentRoomViewController.roomDataSource forceClose:NO]; - } - - [_currentRoomViewController destroy]; - _currentRoomViewController = nil; - } - - _currentRoomViewController = (RoomViewController *)controller; - - if (!_selectedRoomPreviewData) - { - MXKRoomDataSource *roomDataSource; - - // Check whether an event has been selected from messages or files search tab. Live timeline or timeline from a search result? - MXEvent *selectedSearchEvent = messagesSearchViewController.selectedEvent; - MXSession *selectedSearchEventSession = messagesSearchDataSource.mxSession; - if (!selectedSearchEvent) - { - selectedSearchEvent = filesSearchViewController.selectedEvent; - selectedSearchEventSession = filesSearchDataSource.mxSession; - } - - if (!selectedSearchEvent) - { - if (!_selectedEventId) - { - // LIVE: Show the room live timeline managed by MXKRoomDataSourceManager - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:_selectedRoomSession]; - roomDataSource = [roomDataSourceManager roomDataSourceForRoom:_selectedRoomId create:YES]; - } - else - { - // Open the room on the requested event - roomDataSource = [[RoomDataSource alloc] initWithRoomId:_selectedRoomId initialEventId:_selectedEventId andMatrixSession:_selectedRoomSession]; - [roomDataSource finalizeInitialization]; - - // Give the data source ownership to the room view controller. - _currentRoomViewController.hasRoomDataSourceOwnership = YES; - } - } - else - { - // Search result: Create a temp timeline from the selected event - roomDataSource = [[RoomDataSource alloc] initWithRoomId:selectedSearchEvent.roomId initialEventId:selectedSearchEvent.eventId andMatrixSession:selectedSearchEventSession]; - [roomDataSource finalizeInitialization]; - - // Give the data source ownership to the room view controller. - _currentRoomViewController.hasRoomDataSourceOwnership = YES; - } - - [_currentRoomViewController displayRoom:roomDataSource]; - } - else - { - [_currentRoomViewController displayRoomPreview:_selectedRoomPreviewData]; - _selectedRoomPreviewData = nil; - } - } - - if (self.splitViewController) - { - // Refresh selected cell without scrolling the selected cell (We suppose it's visible here) - [self refreshCurrentSelectedCellInChild:NO]; - - // IOS >= 8 - if ([self.splitViewController respondsToSelector:@selector(displayModeButtonItem)]) - { - controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; - } - - // - controller.navigationItem.leftItemsSupplementBackButton = YES; - } - } - else - { - // Keep ref on destinationViewController - [super prepareForSegue:segue sender:sender]; + id cellData = [recentsDataSource cellDataAtIndexPath:[NSIndexPath indexPathForRow:indexPath.item inSection:collectionView.tag]]; - if ([[segue identifier] isEqualToString:@"showDirectory"]) - { - DirectoryViewController *directoryViewController = segue.destinationViewController; - [directoryViewController displayWitDataSource:recentsDataSource.publicRoomsDirectoryDataSource]; - } - else if ([[segue identifier] isEqualToString:@"showContactDetails"]) - { - ContactDetailsViewController *contactDetailsViewController = segue.destinationViewController; - contactDetailsViewController.enableVoipCall = NO; - contactDetailsViewController.contact = selectedContact; - } - else if ([[segue identifier] isEqualToString:@"showAuth"]) - { - // Keep ref on the authentification view controller while it is displayed - // ie until we get the notification about a new account - _authViewController = segue.destinationViewController; - isAuthViewControllerPreparing = NO; - - authViewControllerObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountManagerDidAddAccountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - _authViewController = nil; - - [[NSNotificationCenter defaultCenter] removeObserver:authViewControllerObserver]; - authViewControllerObserver = nil; - }]; - - // Forward parameters if any - if (authViewControllerRegistrationParameters) - { - _authViewController.externalRegistrationParameters = authViewControllerRegistrationParameters; - authViewControllerRegistrationParameters = nil; - } - } + [self.delegate recentListViewController:self didSelectRoom:cellData.roomSummary.roomId inMatrixSession:cellData.roomSummary.room.mxSession]; } - - // Hide back button title - self.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; + + // Hide the keyboard when user select a room + // do not hide the searchBar until the view controller disappear + // on tablets / iphone 6+, the user could expect to search again while looking at a room + [self.recentsSearchBar resignFirstResponder]; } -#pragma mark - Search +#pragma mark - UICollectionViewDelegateFlowLayout -- (void)showSearch:(BOOL)animated +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { - [super showSearch:animated]; - - // Reset searches - [recentsDataSource searchWithPatterns:nil]; - - createNewRoomImageView.hidden = YES; - tableViewMaskLayer.hidden = YES; - - [self updateSearch]; - - // Screen tracking (via Google Analytics) - id tracker = [[GAI sharedInstance] defaultTracker]; - if (tracker) - { - [tracker set:kGAIScreenName value:@"RoomsGlobalSearch"]; - [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; - } -} - -- (void)hideSearch:(BOOL)animated -{ - [super hideSearch:animated]; - - createNewRoomImageView.hidden = self.isHidden; - tableViewMaskLayer.hidden = NO; - self.backgroundImageView.hidden = YES; - - // Reset searches - [recentsDataSource searchWithPatterns:nil]; - - recentsDataSource.hideRecents = NO; - recentsDataSource.hidePublicRoomsDirectory = YES; - - // Screen tracking (via Google Analytics) - id tracker = [[GAI sharedInstance] defaultTracker]; - if (tracker) - { - NSString *currentScreenName = [tracker get:kGAIScreenName]; - if (!currentScreenName || ![currentScreenName isEqualToString:@"RoomsList"]) - { - [tracker set:kGAIScreenName value:@"RoomsList"]; - [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; - } - } -} - -// Update search results under the currently selected tab -- (void)updateSearch -{ - if (self.searchBar.text.length) - { - recentsDataSource.hideRecents = NO; - recentsDataSource.hidePublicRoomsDirectory = NO; - self.backgroundImageView.hidden = YES; - - // Forward the search request to the data source - if (self.selectedViewController == recentsViewController) - { - // Do a AND search on words separated by a space - NSArray *patterns = [self.searchBar.text componentsSeparatedByString:@" "]; - - [recentsDataSource searchWithPatterns:patterns]; - recentsViewController.shouldScrollToTopOnRefresh = YES; - } - else if (self.selectedViewController == messagesSearchViewController) - { - // Launch the search only if the keyboard is no more visible - if (!self.searchBar.isFirstResponder) - { - // Do it asynchronously to give time to messagesSearchViewController to be set up - // so that it can display its loading wheel - dispatch_async(dispatch_get_main_queue(), ^{ - [messagesSearchDataSource searchMessages:self.searchBar.text force:NO]; - messagesSearchViewController.shouldScrollToBottomOnRefresh = YES; - }); - } - } - else if (self.selectedViewController == peopleSearchViewController) - { - [peopleSearchViewController searchWithPattern:self.searchBar.text forceReset:NO complete:^{ - - [self checkAndShowBackgroundImage]; - - }]; - } - else if (self.selectedViewController == filesSearchViewController) - { - // Launch the search only if the keyboard is no more visible - if (!self.searchBar.isFirstResponder) - { - // Do it asynchronously to give time to filesSearchViewController to be set up - // so that it can display its loading wheel - dispatch_async(dispatch_get_main_queue(), ^{ - [filesSearchDataSource searchMessages:self.searchBar.text force:NO]; - filesSearchViewController.shouldScrollToBottomOnRefresh = YES; - }); - } - } - } - else - { - // Nothing to search, show only the public dictionary - recentsDataSource.hideRecents = YES; - recentsDataSource.hidePublicRoomsDirectory = NO; - - // Reset search result (if any) - [recentsDataSource searchWithPatterns:nil]; - if (messagesSearchDataSource.searchText.length) - { - [messagesSearchDataSource searchMessages:nil force:NO]; - } - - [peopleSearchViewController searchWithPattern:nil forceReset:NO complete:^{ - - [self checkAndShowBackgroundImage]; - - }]; - - if (filesSearchDataSource.searchText.length) - { - [filesSearchDataSource searchMessages:nil force:NO]; - } - } - - [self checkAndShowBackgroundImage]; -} - -#pragma mark - UISearchBarDelegate - -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - if (self.selectedViewController == recentsViewController) - { - // As the public room search is local, it can be updated on each text change - [self updateSearch]; - } - else if (self.selectedViewController == peopleSearchViewController) - { - // As the contact search is local, it can be updated on each text change - [self updateSearch]; - } - else if (!self.searchBar.text.length) - { - // Reset message search if any - [self updateSearch]; - } -} - -- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar -{ - [searchBar resignFirstResponder]; - - if (self.selectedViewController == messagesSearchViewController || self.selectedViewController == filesSearchViewController) - { - // As the messages/files search is done homeserver-side, launch it only on the "Search" button - [self updateSearch]; - } -} - -#pragma mark - MXKRecentListViewControllerDelegate - -- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession -{ - // Open the room - [self selectRoomWithId:roomId andEventId:nil inMatrixSession:matrixSession]; -} - -#pragma mark - ContactsTableViewControllerDelegate - -- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact -{ - selectedContact = contact; - - // Force hiding the keyboard - [self.searchBar resignFirstResponder]; - - [self performSegueWithIdentifier:@"showContactDetails" sender:self]; -} - -#pragma mark - Actions - -- (IBAction)onButtonPressed:(id)sender -{ - if (sender == _searchBarButtonIem) - { - [self showSearch:YES]; - } -} - -- (void)onNewRoomPressed -{ - __weak typeof(self) weakSelf = self; - - [currentAlert dismiss:NO]; - - currentAlert = [[MXKAlert alloc] initWithTitle:nil message:nil style:MXKAlertStyleActionSheet]; - - [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"room_recents_start_chat_with", @"Vector", nil) style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - - [strongSelf performSegueWithIdentifier:@"presentStartChat" sender:strongSelf]; - }]; - - [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"room_recents_create_empty_room", @"Vector", nil) style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { - - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - - [strongSelf createEmptyRoom]; - }]; - - currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:MXKAlertActionStyleCancel handler:^(MXKAlert *alert) { - - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - }]; - - currentAlert.sourceView = createNewRoomImageView; - - currentAlert.mxkAccessibilityIdentifier = @"HomeVCCreateRoomAlert"; - [currentAlert showInViewController:self]; -} - -- (void)createEmptyRoom -{ - // Sanity check - if (self.mainSession) - { - // Create one room at time - if (!roomCreationRequest) - { - [recentsViewController startActivityIndicator]; - - // Create an empty room. - roomCreationRequest = [self.mainSession createRoom:nil - visibility:kMXRoomDirectoryVisibilityPrivate - roomAlias:nil - topic:nil - success:^(MXRoom *room) { - - roomCreationRequest = nil; - [recentsViewController stopActivityIndicator]; - if (currentAlert) - { - [currentAlert dismiss:NO]; - currentAlert = nil; - } - - [self selectRoomWithId:room.state.roomId andEventId:nil inMatrixSession:self.mainSession]; - - // Force the expanded header - self.currentRoomViewController.showExpandedHeader = YES; - - } failure:^(NSError *error) { - - roomCreationRequest = nil; - [recentsViewController stopActivityIndicator]; - if (currentAlert) - { - [currentAlert dismiss:NO]; - currentAlert = nil; - } - - NSLog(@"[HomeViewController] Create new room failed"); - - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - else - { - // Ask the user to wait - __weak __typeof(self) weakSelf = self; - currentAlert = [[MXKAlert alloc] initWithTitle:nil - message:NSLocalizedStringFromTable(@"room_creation_wait_for_creation", @"Vector", nil) - style:MXKAlertStyleAlert]; - - currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] - style:MXKAlertActionStyleCancel - handler:^(MXKAlert *alert) { - - __strong __typeof(weakSelf)strongSelf = weakSelf; - strongSelf->currentAlert = nil; - - }]; - currentAlert.mxkAccessibilityIdentifier = @"HomeVCRoomCreationInProgressAlert"; - [currentAlert showInViewController:self]; - } - } + return [RoomCollectionViewCell defaultCellSize]; } @end diff --git a/Riot/ViewController/MasterTabBarController.h b/Riot/ViewController/MasterTabBarController.h new file mode 100644 index 000000000..348d12ebc --- /dev/null +++ b/Riot/ViewController/MasterTabBarController.h @@ -0,0 +1,132 @@ +/* + Copyright 2017 Vector Creations 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 "AuthenticationViewController.h" + +#import "HomeViewController.h" +#import "FavouritesViewController.h" +#import "PeopleViewController.h" +#import "RoomsViewController.h" + +#import "RoomViewController.h" +#import "ContactDetailsViewController.h" + +#define TABBAR_HOME_INDEX 0 +#define TABBAR_FAVOURITES_INDEX 1 +#define TABBAR_PEOPLE_INDEX 2 +#define TABBAR_ROOMS_INDEX 3 +#define TABBAR_COUNT 4 + +@interface MasterTabBarController : UITabBarController + +@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsBarButtonItem; +@property (weak, nonatomic) IBOutlet UIBarButtonItem *searchBarButtonIem; + +// Associated matrix sessions (empty by default). +@property (nonatomic, readonly) NSArray *mxSessions; + +// Add a matrix session. This session is propagated to all view controllers handled by the tab bar controller. +- (void)addMatrixSession:(MXSession*)mxSession; +// Remove a matrix session. +- (void)removeMatrixSession:(MXSession*)mxSession; + +/** + Display the authentication screen. + */ +- (void)showAuthenticationScreen; + +/** + Display the authentication screen in order to pursue a registration process by using a predefined set + of parameters. + + If the provided registration parameters are not supported, we switch back to the default login screen. + + @param parameters the set of parameters. + */ +- (void)showAuthenticationScreenWithRegistrationParameters:(NSDictionary*)parameters; + +/** + Open the room with the provided identifier in a specific matrix session. + + @param roomId the room identifier. + @param eventId if not nil, the room will be opened on this event. + @param mxSession the matrix session in which the room should be available. + */ +- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)mxSession; + +/** + Open the RoomViewController to display the preview of a room that is unknown for the user. + + This room can come from an email invitation link or a simple link to a room. + + @param roomPreviewData the data for the room preview. + */ +- (void)showRoomPreview:(RoomPreviewData*)roomPreviewData; + +/** + Open a ContactDetailsViewController to display the information of the provided contact. + */ +- (void)selectContact:(MXKContact*)contact; + +/** + Release the current selected item (if any). + */ +- (void)releaseSelectedItem; + +/** + Dismiss the unified search screen (if any). + */ +- (void)dismissUnifiedSearch:(BOOL)animated completion:(void (^)(void))completion; + +/** + The current number of rooms with missed notifications, including the invites. + */ +- (NSUInteger)missedDiscussionsCount; + +/** + The current number of rooms with unread highlighted messages. + */ +- (NSUInteger)missedHighlightDiscussionsCount; + +/** + Refresh the missed conversations badges on tab bar icon + */ +- (void)refreshTabBarBadges; + + +// Reference to the current auth VC. It is not nil only when the auth screen is displayed. +@property (nonatomic, readonly) AuthenticationViewController *authViewController; + +@property (nonatomic, readonly) HomeViewController *homeViewController; +@property (nonatomic, readonly) FavouritesViewController *favouritesViewController; +@property (nonatomic, readonly) PeopleViewController *peopleViewController; +@property (nonatomic, readonly) RoomsViewController *roomsViewController; + +// References on the currently selected room and its view controller +@property (nonatomic, readonly) RoomViewController *currentRoomViewController; +@property (nonatomic, readonly) NSString *selectedRoomId; +@property (nonatomic, readonly) NSString *selectedEventId; +@property (nonatomic, readonly) MXSession *selectedRoomSession; +@property (nonatomic, readonly) RoomPreviewData *selectedRoomPreviewData; + +// References on the currently selected contact and its view controller +@property (nonatomic, readonly) ContactDetailsViewController *currentContactDetailViewController; +@property (nonatomic, readonly) MXKContact *selectedContact; + +@end + diff --git a/Riot/ViewController/MasterTabBarController.m b/Riot/ViewController/MasterTabBarController.m new file mode 100644 index 000000000..3155501a5 --- /dev/null +++ b/Riot/ViewController/MasterTabBarController.m @@ -0,0 +1,747 @@ +/* + Copyright 2017 Vector Creations 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 "UnifiedSearchViewController.h" + +#import "RecentsDataSource.h" + +#import "AppDelegate.h" + +#import "MXRoom+Riot.h" + +@interface MasterTabBarController () +{ + // Array of `MXSession` instances. + NSMutableArray *mxSessionArray; + + // Tell whether the authentication screen is preparing. + BOOL isAuthViewControllerPreparing; + + // Observer that checks when the Authentification view controller has gone. + id authViewControllerObserver; + + // The parameters to pass to the Authentification view controller. + NSDictionary *authViewControllerRegistrationParameters; + + // The recents data source shared between all the view controllers of the tab bar. + RecentsDataSource *recentsDataSource; + + // The current unified search screen if any + UnifiedSearchViewController *unifiedSearchViewController; + + // Current alert (if any). + MXKAlert *currentAlert; +} + +@property(nonatomic,getter=isHidden) BOOL hidden; + +@end + +@implementation MasterTabBarController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + + // Retrieve the all view controllers + _homeViewController = [self.viewControllers objectAtIndex:TABBAR_HOME_INDEX]; + _favouritesViewController = [self.viewControllers objectAtIndex:TABBAR_FAVOURITES_INDEX]; + _peopleViewController = [self.viewControllers objectAtIndex:TABBAR_PEOPLE_INDEX]; + _roomsViewController = [self.viewControllers objectAtIndex:TABBAR_ROOMS_INDEX]; + + // Sanity check + NSAssert(_homeViewController && _favouritesViewController && _peopleViewController && _roomsViewController, @"Something wrong in Main.storyboard"); + + self.tabBar.tintColor = kRiotColorGreen; + + // Adjust the display of the icons in the tabbar. + for (UITabBarItem *tabBarItem in self.tabBar.items) + { + tabBarItem.imageInsets = UIEdgeInsetsMake(5, 0, -5, 0); + } + + // Initialize here the data sources if a matrix session has been already set. + [self initializeDataSources]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Show the tab bar view controller content only when a user is logged in. + self.hidden = ([MXKAccountManager sharedManager].accounts.count == 0); +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + // Check whether we're not logged in + if (![MXKAccountManager sharedManager].accounts.count) + { + [self showAuthenticationScreen]; + } + else + { + // Check whether the user has been already prompted to send crash reports. + // (Check whether 'enableCrashReport' flag has been set once) + if (![[NSUserDefaults standardUserDefaults] objectForKey:@"enableCrashReport"]) + { + [self promptUserBeforeUsingGoogleAnalytics]; + } + + [self refreshTabBarBadges]; + } + + if (unifiedSearchViewController) + { + [unifiedSearchViewController destroy]; + unifiedSearchViewController = nil; + } +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; +} + +- (void)dealloc +{ + mxSessionArray = nil; + + _homeViewController = nil; + _favouritesViewController = nil; + _peopleViewController = nil; + _roomsViewController = nil; + + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + if (authViewControllerObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:authViewControllerObserver]; + authViewControllerObserver = nil; + } +} + +#pragma mark - + +- (NSArray*)mxSessions +{ + return [NSArray arrayWithArray:mxSessionArray]; +} + +- (void)initializeDataSources +{ + MXSession *mainSession = mxSessionArray.firstObject; + + if (mainSession) + { + NSLog(@"[MasterTabBarController] initializeDataSources"); + + // Init the recents data source + recentsDataSource = [[RecentsDataSource alloc] initWithMatrixSession:mainSession]; + + [_homeViewController displayList:recentsDataSource]; + [_favouritesViewController displayList:recentsDataSource]; + [_peopleViewController displayList:recentsDataSource]; + [_roomsViewController displayList:recentsDataSource]; + + // Restore the right delegate of the shared recent data source. + id recentsDataSourceDelegate = _homeViewController; + RecentsDataSourceMode recentsDataSourceMode = RecentsDataSourceModeHome; + switch (self.selectedIndex) + { + case TABBAR_HOME_INDEX: + break; + case TABBAR_FAVOURITES_INDEX: + recentsDataSourceDelegate = _favouritesViewController; + recentsDataSourceMode = RecentsDataSourceModeFavourites; + break; + case TABBAR_PEOPLE_INDEX: + recentsDataSourceDelegate = _peopleViewController; + recentsDataSourceMode = RecentsDataSourceModePeople; + break; + case TABBAR_ROOMS_INDEX: + recentsDataSourceDelegate = _roomsViewController; + recentsDataSourceMode = RecentsDataSourceModeRooms; + break; + + default: + break; + } + [recentsDataSource setDelegate:recentsDataSourceDelegate andRecentsDataSourceMode:recentsDataSourceMode]; + + // Check whether there are others sessions + NSArray* mxSessions = self.mxSessions; + if (mxSessions.count > 1) + { + for (MXSession *mxSession in mxSessions) + { + if (mxSession != mainSession) + { + // Add the session to the recents data source + [recentsDataSource addMatrixSession:mxSession]; + } + } + } + } +} + +- (void)addMatrixSession:(MXSession *)mxSession +{ + // Check whether the controller'€™s view is loaded into memory. + if (_homeViewController) + { + // Check whether the data sources have been initialized. + if (!recentsDataSource) + { + // Add first the session. The updated sessions list will be used during data sources initialization. + mxSessionArray = [NSMutableArray array]; + [mxSessionArray addObject:mxSession]; + + // Prepare data sources and return + [self initializeDataSources]; + return; + } + else + { + // Add the session to the existing data sources + [recentsDataSource addMatrixSession:mxSession]; + } + } + + if (!mxSessionArray) + { + mxSessionArray = [NSMutableArray array]; + + // Add matrix sessions observer on first added session + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil]; + } + [mxSessionArray addObject:mxSession]; +} + +- (void)removeMatrixSession:(MXSession *)mxSession +{ + [recentsDataSource removeMatrixSession:mxSession]; + + // Check whether there are others sessions + if (!recentsDataSource.mxSessions.count) + { + // Remove matrix sessions observer + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil]; + + [_homeViewController displayList:nil]; + [_favouritesViewController displayList:nil]; + [_peopleViewController displayList:nil]; + [_roomsViewController displayList:nil]; + + [recentsDataSource destroy]; + recentsDataSource = nil; + } + + [mxSessionArray removeObject:mxSession]; +} + +- (void)onMatrixSessionStateDidChange:(NSNotification *)notif +{ + [self refreshTabBarBadges]; +} + +- (void)showAuthenticationScreen +{ + // Check whether an authentication screen is not already shown or preparing + if (!self.authViewController && !isAuthViewControllerPreparing) + { + isAuthViewControllerPreparing = YES; + + [[AppDelegate theDelegate] restoreInitialDisplay:^{ + + [self performSegueWithIdentifier:@"showAuth" sender:self]; + + }]; + } +} + +- (void)showAuthenticationScreenWithRegistrationParameters:(NSDictionary *)parameters +{ + if (self.authViewController) + { + NSLog(@"[MasterTabBarController] Universal link: Forward registration parameter to the existing AuthViewController"); + self.authViewController.externalRegistrationParameters = parameters; + } + else + { + NSLog(@"[MasterTabBarController] Universal link: Logout current sessions and open AuthViewController to complete the registration"); + + // Keep a ref on the params + authViewControllerRegistrationParameters = parameters; + + // And do a logout out. It will then display AuthViewController + [[AppDelegate theDelegate] logout]; + } +} + +- (void)selectRoomWithId:(NSString*)roomId andEventId:(NSString*)eventId inMatrixSession:(MXSession*)matrixSession +{ + if (_selectedRoomId && [_selectedRoomId isEqualToString:roomId] + && _selectedEventId && [_selectedEventId isEqualToString:eventId] + && _selectedRoomSession && _selectedRoomSession == matrixSession) + { + // Nothing to do + return; + } + + _selectedRoomId = roomId; + _selectedEventId = eventId; + _selectedRoomSession = matrixSession; + + if (roomId && matrixSession) + { + [self performSegueWithIdentifier:@"showRoomDetails" sender:self]; + } + else + { + [self releaseSelectedItem]; + } +} + +- (void)showRoomPreview:(RoomPreviewData *)roomPreviewData +{ + _selectedRoomPreviewData = roomPreviewData; + _selectedRoomId = roomPreviewData.roomId; + _selectedRoomSession = roomPreviewData.mxSession; + + [self performSegueWithIdentifier:@"showRoomDetails" sender:self]; +} + +- (void)selectContact:(MXKContact*)contact +{ + _selectedContact = contact; + + [self performSegueWithIdentifier:@"showContactDetails" sender:self]; +} + +- (void)releaseSelectedItem +{ + _selectedRoomId = nil; + _selectedEventId = nil; + _selectedRoomSession = nil; + + if (_currentRoomViewController) + { + // If the displayed data is not a preview, let the manager release the room data source + // (except if the view controller has the room data source ownership). + if (!_currentRoomViewController.roomPreviewData && _currentRoomViewController.roomDataSource && !_currentRoomViewController.hasRoomDataSourceOwnership) + { + MXSession *mxSession = _currentRoomViewController.roomDataSource.mxSession; + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession]; + + // Let the manager release live room data sources where the user is in + [roomDataSourceManager closeRoomDataSource:_currentRoomViewController.roomDataSource forceClose:NO]; + } + + [_currentRoomViewController destroy]; + _currentRoomViewController = nil; + } + + _selectedContact = nil; + + if (_currentContactDetailViewController) + { + [_currentContactDetailViewController destroy]; + _currentContactDetailViewController = nil; + } +} + +- (void)dismissUnifiedSearch:(BOOL)animated completion:(void (^)(void))completion +{ + if (unifiedSearchViewController) + { + [self.navigationController dismissViewControllerAnimated:animated completion:completion]; + } + else if (completion) + { + completion(); + } +} + +- (NSUInteger)missedDiscussionsCount +{ + NSUInteger roomCount = 0; + + // Considering all the current sessions. + for (MXSession *session in mxSessionArray) + { + // Sum all the rooms with missed notifications. + for (MXRoomSummary *roomSummary in session.roomsSummaries) + { + NSUInteger notificationCount = roomSummary.notificationCount; + + // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. + if (roomSummary.room.isMentionsOnly) + { + // Only the highlighted missed messages must be considered here. + notificationCount = roomSummary.highlightCount; + } + + if (notificationCount) + { + roomCount ++; + } + } + + // Add the invites count + roomCount += [session invitedRooms].count; + } + + return roomCount; +} + +- (NSUInteger)missedHighlightDiscussionsCount +{ + NSUInteger roomCount = 0; + + for (MXSession *session in mxSessionArray) + { + roomCount += [session missedHighlightDiscussionsCount]; + } + + return roomCount; +} + +#pragma mark - + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender +{ + if ([[segue identifier] isEqualToString:@"showRoomDetails"] || [[segue identifier] isEqualToString:@"showContactDetails"]) + { + UINavigationController *navigationController = [segue destinationViewController]; + + // Release existing Room view controller (if any) + if (_currentRoomViewController) + { + // If the displayed data is not a preview, let the manager release the room data source + // (except if the view controller has the room data source ownership). + if (!_currentRoomViewController.roomPreviewData && _currentRoomViewController.roomDataSource && !_currentRoomViewController.hasRoomDataSourceOwnership) + { + MXSession *mxSession = _currentRoomViewController.roomDataSource.mxSession; + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession]; + + [roomDataSourceManager closeRoomDataSource:_currentRoomViewController.roomDataSource forceClose:NO]; + } + + [_currentRoomViewController destroy]; + _currentRoomViewController = nil; + } + else if (_currentContactDetailViewController) + { + [_currentContactDetailViewController destroy]; + _currentContactDetailViewController = nil; + } + + if ([[segue identifier] isEqualToString:@"showRoomDetails"]) + { + // Replace the rootviewcontroller with a room view controller + // Get the RoomViewController from the storyboard + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; + _currentRoomViewController = [storyboard instantiateViewControllerWithIdentifier:@"RoomViewControllerStoryboardId"]; + + navigationController.viewControllers = @[_currentRoomViewController]; + + if (!_selectedRoomPreviewData) + { + MXKRoomDataSource *roomDataSource; + + // Check whether an event has been selected from messages or files search tab. + MXEvent *selectedSearchEvent = unifiedSearchViewController.selectedSearchEvent; + MXSession *selectedSearchEventSession = unifiedSearchViewController.selectedSearchEventSession; + + if (!selectedSearchEvent) + { + if (!_selectedEventId) + { + // LIVE: Show the room live timeline managed by MXKRoomDataSourceManager + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:_selectedRoomSession]; + roomDataSource = [roomDataSourceManager roomDataSourceForRoom:_selectedRoomId create:YES]; + } + else + { + // Open the room on the requested event + roomDataSource = [[RoomDataSource alloc] initWithRoomId:_selectedRoomId initialEventId:_selectedEventId andMatrixSession:_selectedRoomSession]; + [roomDataSource finalizeInitialization]; + + ((RoomDataSource*)roomDataSource).markTimelineInitialEvent = YES; + + // Give the data source ownership to the room view controller. + _currentRoomViewController.hasRoomDataSourceOwnership = YES; + } + } + else + { + // Search result: Create a temp timeline from the selected event + roomDataSource = [[RoomDataSource alloc] initWithRoomId:selectedSearchEvent.roomId initialEventId:selectedSearchEvent.eventId andMatrixSession:selectedSearchEventSession]; + [roomDataSource finalizeInitialization]; + + ((RoomDataSource*)roomDataSource).markTimelineInitialEvent = YES; + + // Give the data source ownership to the room view controller. + _currentRoomViewController.hasRoomDataSourceOwnership = YES; + } + + [_currentRoomViewController displayRoom:roomDataSource]; + } + else + { + [_currentRoomViewController displayRoomPreview:_selectedRoomPreviewData]; + _selectedRoomPreviewData = nil; + } + } + else + { + // Replace the rootviewcontroller with a contact details view controller + _currentContactDetailViewController = [ContactDetailsViewController contactDetailsViewController]; + _currentContactDetailViewController.enableVoipCall = NO; + _currentContactDetailViewController.contact = _selectedContact; + + navigationController.viewControllers = @[_currentContactDetailViewController]; + } + + if (self.splitViewController) + { + // Refresh selected cell without scrolling the selected cell (We suppose it's visible here) + [self refreshCurrentSelectedCell:NO]; + + if (_currentRoomViewController) + { + _currentRoomViewController.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; + _currentRoomViewController.navigationItem.leftItemsSupplementBackButton = YES; + } + else + { + _currentContactDetailViewController.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; + _currentContactDetailViewController.navigationItem.leftItemsSupplementBackButton = YES; + } + } + } + else + { + // Keep ref on destinationViewController + [super prepareForSegue:segue sender:sender]; + + if ([[segue identifier] isEqualToString:@"showAuth"]) + { + // Keep ref on the authentification view controller while it is displayed + // ie until we get the notification about a new account + _authViewController = segue.destinationViewController; + isAuthViewControllerPreparing = NO; + + authViewControllerObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountManagerDidAddAccountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + + _authViewController = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:authViewControllerObserver]; + authViewControllerObserver = nil; + }]; + + // Forward parameters if any + if (authViewControllerRegistrationParameters) + { + _authViewController.externalRegistrationParameters = authViewControllerRegistrationParameters; + authViewControllerRegistrationParameters = nil; + } + } + else if ([[segue identifier] isEqualToString:@"showUnifiedSearch"]) + { + unifiedSearchViewController= segue.destinationViewController; + + for (MXSession *session in mxSessionArray) + { + [unifiedSearchViewController addMatrixSession:session]; + } + } + } + + // Hide back button title + self.navigationController.topViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; +} + +// Made the actual selected view controller update its selected cell. +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible +{ + UIViewController *selectedViewController = self.selectedViewController; + + if ([selectedViewController respondsToSelector:@selector(refreshCurrentSelectedCell:)]) + { + [(id)selectedViewController refreshCurrentSelectedCell:forceVisible]; + } +} + +- (void)setHidden:(BOOL)hidden +{ + _hidden = hidden; + + [self.view superview].backgroundColor = [UIColor whiteColor]; + self.view.hidden = hidden; + self.navigationController.navigationBar.hidden = hidden; +} + +#pragma mark - + +- (void)refreshTabBarBadges +{ + // Use a middle dot to signal missed notif in favourites + [self setMissedDiscussionsMark:(recentsDataSource.missedFavouriteDiscussionsCount? @"\u00B7": nil) onTabBarItem:TABBAR_FAVOURITES_INDEX withBadgeColor:(recentsDataSource.missedHighlightFavouriteDiscussionsCount ? kRiotColorPinkRed : kRiotColorGreen)]; + + // Update the badge on People and Rooms tabs + [self setMissedDiscussionsCount:recentsDataSource.missedDirectDiscussionsCount onTabBarItem:TABBAR_PEOPLE_INDEX withBadgeColor:(recentsDataSource.missedHighlightDirectDiscussionsCount ? kRiotColorPinkRed : kRiotColorGreen)]; + [self setMissedDiscussionsCount:recentsDataSource.missedGroupDiscussionsCount onTabBarItem:TABBAR_ROOMS_INDEX withBadgeColor:(recentsDataSource.missedHighlightGroupDiscussionsCount ? kRiotColorPinkRed : kRiotColorGreen)]; +} + +- (void)setMissedDiscussionsCount:(NSUInteger)count onTabBarItem:(NSUInteger)index withBadgeColor:(UIColor*)badgeColor +{ + if (count) + { + NSString *badgeValue = [self tabBarBadgeStringValue:count]; + + self.tabBar.items[index].badgeValue = badgeValue; + + if ([UITabBarItem instancesRespondToSelector:@selector(setBadgeColor:)]) + { + self.tabBar.items[index].badgeColor = badgeColor; + } + } + else + { + self.tabBar.items[index].badgeValue = nil; + } +} + +- (void)setMissedDiscussionsMark:(NSString*)mark onTabBarItem:(NSUInteger)index withBadgeColor:(UIColor*)badgeColor +{ + if (mark) + { + self.tabBar.items[index].badgeValue = mark; + + if ([UITabBarItem instancesRespondToSelector:@selector(setBadgeColor:)]) + { + self.tabBar.items[index].badgeColor = badgeColor; + } + } + else + { + self.tabBar.items[index].badgeValue = nil; + } +} + +- (NSString*)tabBarBadgeStringValue:(NSUInteger)count +{ + NSString *badgeValue; + + if (count > 1000) + { + CGFloat value = count / 1000.0; + badgeValue = [NSString stringWithFormat:NSLocalizedStringFromTable(@"large_badge_value_k_format", @"Vector", nil), value]; + } + else + { + badgeValue = [NSString stringWithFormat:@"%tu", count]; + } + + return badgeValue; +} + +#pragma mark - + +- (void)promptUserBeforeUsingGoogleAnalytics +{ + NSLog(@"[MasterTabBarController]: Invite the user to send crash reports"); + + __weak typeof(self) weakSelf = self; + + [currentAlert dismiss:NO]; + + NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]; + + currentAlert = [[MXKAlert alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedStringFromTable(@"google_analytics_use_prompt", @"Vector", nil), appDisplayName] + message:nil + style:MXKAlertStyleAlert]; + + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"] + style:MXKAlertActionStyleDefault + handler:^(MXKAlert *alert) { + + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"enableCrashReport"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + if (weakSelf) + { + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + } + + }]; + [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"] + style:MXKAlertActionStyleDefault + handler:^(MXKAlert *alert) { + + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"enableCrashReport"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + if (weakSelf) + { + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + } + + [[AppDelegate theDelegate] startGoogleAnalytics]; + + }]; + + currentAlert.mxkAccessibilityIdentifier = @"HomeVCUseGoogleAnalyticsAlert"; + [currentAlert showInViewController:self]; +} + +#pragma mark - UITabBarDelegate + +- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item +{ + // Detect multi-tap on the current selected tab. + if (item.tag == self.selectedIndex) + { + // Scroll to the next room with missed notifications. + if (item.tag == TABBAR_ROOMS_INDEX) + { + [self.roomsViewController scrollToNextRoomWithMissedNotifications]; + } + else if (item.tag == TABBAR_PEOPLE_INDEX) + { + [self.peopleViewController scrollToNextRoomWithMissedNotifications]; + } + else if (item.tag == TABBAR_FAVOURITES_INDEX) + { + [self.favouritesViewController scrollToNextRoomWithMissedNotifications]; + } + } +} + +@end diff --git a/Riot/ViewController/MediaAlbumContentViewController.m b/Riot/ViewController/MediaAlbumContentViewController.m index 2917186f3..e7ae70b28 100644 --- a/Riot/ViewController/MediaAlbumContentViewController.m +++ b/Riot/ViewController/MediaAlbumContentViewController.m @@ -19,10 +19,6 @@ #import "AppDelegate.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import @interface MediaAlbumContentViewController () diff --git a/Riot/ViewController/MediaPickerViewController.m b/Riot/ViewController/MediaPickerViewController.m index 6cf0bf0b6..a07a1bb86 100644 --- a/Riot/ViewController/MediaPickerViewController.m +++ b/Riot/ViewController/MediaPickerViewController.m @@ -29,9 +29,7 @@ #import "MediaAlbumTableCell.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" +#import "MXKPieChartView.h" static void *CapturingStillImageContext = &CapturingStillImageContext; static void *RecordingContext = &RecordingContext; @@ -1114,27 +1112,29 @@ static void *RecordingContext = &RecordingContext; - (void)tearDownAVCapture { - frontCameraInput = nil; - backCameraInput = nil; - captureSession = nil; - - if (movieFileOutput) - { - [movieFileOutput removeObserver:self forKeyPath:@"recording" context:RecordingContext]; - movieFileOutput = nil; - } - - if (stillImageOutput) - { - [stillImageOutput removeObserver:self forKeyPath:@"capturingStillImage" context:CapturingStillImageContext]; - stillImageOutput = nil; - } - - currentCameraInput = nil; - - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionRuntimeErrorNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStartRunningNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStopRunningNotification object:nil]; + dispatch_sync(cameraQueue, ^{ + frontCameraInput = nil; + backCameraInput = nil; + captureSession = nil; + + if (movieFileOutput) + { + [movieFileOutput removeObserver:self forKeyPath:@"recording" context:RecordingContext]; + movieFileOutput = nil; + } + + if (stillImageOutput) + { + [stillImageOutput removeObserver:self forKeyPath:@"capturingStillImage" context:CapturingStillImageContext]; + stillImageOutput = nil; + } + + currentCameraInput = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionRuntimeErrorNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStartRunningNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStopRunningNotification object:nil]; + }); } - (void)caughtAVRuntimeError:(NSNotification*)note diff --git a/Riot/ViewController/PeopleViewController.h b/Riot/ViewController/PeopleViewController.h new file mode 100644 index 000000000..651df48e3 --- /dev/null +++ b/Riot/ViewController/PeopleViewController.h @@ -0,0 +1,31 @@ +/* + Copyright 2017 Vector Creations 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 "ContactsDataSource.h" + +/** + 'PeopleViewController' instance is used to display/filter the direct rooms and a list of contacts. + */ +@interface PeopleViewController : RecentsViewController + +/** + Scroll the next room with missed notifications to the top. + */ +- (void)scrollToNextRoomWithMissedNotifications; + +@end + diff --git a/Riot/ViewController/PeopleViewController.m b/Riot/ViewController/PeopleViewController.m new file mode 100644 index 000000000..52ba8fb49 --- /dev/null +++ b/Riot/ViewController/PeopleViewController.m @@ -0,0 +1,457 @@ +/* + Copyright 2017 Vector Creations 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 "PeopleViewController.h" + +#import "UIViewController+RiotSearch.h" + +#import "RageShakeManager.h" + +#import "AppDelegate.h" + +#import "RecentsDataSource.h" +#import "RecentTableViewCell.h" +#import "InviteRecentTableViewCell.h" + +#import "ContactTableViewCell.h" + +@interface PeopleViewController () +{ + NSInteger directRoomsSectionNumber; + + ContactsDataSource *contactsDataSource; + NSInteger contactsSectionNumber; + + RecentsDataSource *recentsDataSource; +} + +@end + +@implementation PeopleViewController + +- (void)finalizeInit +{ + [super finalizeInit]; + + directRoomsSectionNumber = 0; + contactsSectionNumber = 0; + + self.screenName = @"People"; + + // Prepare its contacts data source + contactsDataSource = [[ContactsDataSource alloc] init]; + contactsDataSource.contactCellAccessoryType = UITableViewCellAccessoryDisclosureIndicator; + contactsDataSource.delegate = self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + + self.view.accessibilityIdentifier = @"PeopleVCView"; + self.recentsTableView.accessibilityIdentifier = @"PeopleVCTableView"; + + // Tag the recents table with the its recents data source mode. + // This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods). + self.recentsTableView.tag = RecentsDataSourceModePeople; + + // Add the (+) button programmatically + [self addPlusButton]; + + // Apply tintColor on the (+) button + plusButtonImageView.image = [UIImage imageNamed:@"create_direct_chat"]; + + // Register table view cell for contacts. + [self.recentsTableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:ContactTableViewCell.defaultReuseIdentifier]; + + // Change the table data source. It must be the people view controller itself. + self.recentsTableView.dataSource = self; + + self.enableStickyHeaders = YES; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)destroy +{ + contactsDataSource.delegate = nil; + [contactsDataSource destroy]; + contactsDataSource = nil; + + [super destroy]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Check whether the access to the local contacts has not been already asked. + if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) + { + // Allow by default the local contacts sync in order to discover matrix users. + // This setting change will trigger the loading of the local contacts, which will automatically + // ask user permission to access their local contacts. + [MXKAppSettings standardAppSettings].syncLocalContacts = YES; + } + + [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_people", @"Vector", nil); + [AppDelegate theDelegate].masterTabBarController.navigationController.navigationBar.tintColor = kRiotColorOrange; + [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = kRiotColorOrange; + + if (recentsDataSource) + { + // Take the lead on the shared data source. + recentsDataSource.areSectionsShrinkable = NO; + [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModePeople]; + } +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + if ([AppDelegate theDelegate].masterTabBarController.tabBar.tintColor == kRiotColorOrange) + { + // Restore default tintColor + [AppDelegate theDelegate].masterTabBarController.navigationController.navigationBar.tintColor = kRiotColorGreen; + [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = kRiotColorGreen; + } +} + +#pragma mark - + +- (void)displayList:(MXKRecentsDataSource *)listDataSource +{ + [super displayList:listDataSource]; + + // Change the table data source. It must be the people view controller itself. + self.recentsTableView.dataSource = self; + + // Keep a ref on the recents data source + if ([listDataSource isKindOfClass:RecentsDataSource.class]) + { + recentsDataSource = (RecentsDataSource*)listDataSource; + } + +} + +#pragma mark - MXKDataSourceDelegate + +- (Class)cellViewClassForCellData:(MXKCellData*)cellData +{ + if ([cellData isKindOfClass:MXKContact.class]) + { + return ContactTableViewCell.class; + } + + return [super cellViewClassForCellData:cellData]; +} + +#pragma mark - UITableView data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + // Retrieve the current number of sections related to the direct rooms. + // Sanity check: check whether the recents data source is correctly configured. + directRoomsSectionNumber = 0; + + if (recentsDataSource.recentsDataSourceMode == RecentsDataSourceModePeople) + { + directRoomsSectionNumber = [self.dataSource numberOfSectionsInTableView:self.recentsTableView]; + } + + // Retrieve the current number of sections related to the contacts + contactsSectionNumber = [contactsDataSource numberOfSectionsInTableView:self.recentsTableView]; + + return (directRoomsSectionNumber + contactsSectionNumber); +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + NSInteger count = 0; + + if (section < directRoomsSectionNumber) + { + count = [self.dataSource tableView:tableView numberOfRowsInSection:section]; + } + else + { + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + count = [contactsDataSource tableView:tableView numberOfRowsInSection:section]; + } + } + + return count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger section = indexPath.section; + + if (section < directRoomsSectionNumber) + { + return [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath]; + } + else + { + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + return [contactsDataSource tableView:tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; + } + } + + return nil; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger section = indexPath.section; + + if (section < directRoomsSectionNumber) + { + return [self.dataSource tableView:tableView canEditRowAtIndexPath:indexPath]; + } + else + { + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + return [contactsDataSource tableView:tableView canEditRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; + } + } + + return NO; +} + +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + if (section >= directRoomsSectionNumber) + { + // Let the contact dataSource provide the height of the section header. + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + return [contactsDataSource heightForHeaderInSection:section]; + } + else + { + return 0.0; + } + } + + return [super tableView:tableView heightForHeaderInSection:section]; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + if (section >= directRoomsSectionNumber) + { + // Let the contact dataSource provide the section header. + CGRect frame = [tableView rectForHeaderInSection:section]; + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + UIView *sectionHeader = [contactsDataSource viewForHeaderInSection:section withFrame:frame]; + sectionHeader.tag = section + directRoomsSectionNumber; + + if (self.enableStickyHeaders) + { + while (sectionHeader.gestureRecognizers.count) + { + UIGestureRecognizer *gestureRecognizer = sectionHeader.gestureRecognizers.lastObject; + [sectionHeader removeGestureRecognizer:gestureRecognizer]; + } + + // Handle tap gesture + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnSectionHeader:)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [sectionHeader addGestureRecognizer:tap]; + } + + return sectionHeader; + } + else + { + return nil; + } + } + + return [super tableView:tableView viewForHeaderInSection:section]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger section = indexPath.section; + if (section >= directRoomsSectionNumber) + { + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + if ([contactsDataSource contactAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]) + { + // Return the default height of the contact cell + return 74.0; + } + + return 50; + } + else + { + return 0.0; + } + } + + return [super tableView:tableView heightForRowAtIndexPath:indexPath]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSInteger section = indexPath.section; + if (section >= directRoomsSectionNumber) + { + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + MXKContact *mxkContact = [contactsDataSource contactAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; + + if (mxkContact) + { + [[AppDelegate theDelegate].masterTabBarController selectContact:mxkContact]; + + // Keep selected the cell by default. + return; + } + } + else + { + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + return; + } + } + + return [super tableView:tableView didSelectRowAtIndexPath:indexPath]; +} + +#pragma mark - Override RecentsViewController + +- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section +{ + CGRect frame = [tableView rectForHeaderInSection:section]; + frame.size.height = self.stickyHeaderHeight; + + if (section >= directRoomsSectionNumber) + { + // Let the contact dataSource provide this header. + section -= directRoomsSectionNumber; + if (section < contactsSectionNumber) + { + return [contactsDataSource viewForStickyHeaderInSection:section withFrame:frame]; + } + } + else if (recentsDataSource) + { + return [recentsDataSource viewForStickyHeaderInSection:section withFrame:frame]; + } + + return nil; +} + +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible +{ + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode != RecentsDataSourceModePeople) + { + return; + } + + // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. + NSIndexPath *currentSelectedCellIndexPath = nil; + MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; + if (masterTabBarController.currentContactDetailViewController) + { + // Look for the rank of this selected contact + currentSelectedCellIndexPath = [contactsDataSource cellIndexPathWithContact:masterTabBarController.selectedContact]; + + if (currentSelectedCellIndexPath) + { + // Select the right row + currentSelectedCellIndexPath = [NSIndexPath indexPathForRow:currentSelectedCellIndexPath.row inSection:(directRoomsSectionNumber + currentSelectedCellIndexPath.section)]; + [self.recentsTableView selectRowAtIndexPath:currentSelectedCellIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; + + if (forceVisible) + { + // Scroll table view to make the selected row appear at second position + NSInteger topCellIndexPathRow = currentSelectedCellIndexPath.row ? currentSelectedCellIndexPath.row - 1: currentSelectedCellIndexPath.row; + NSIndexPath* indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:currentSelectedCellIndexPath.section]; + [self.recentsTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; + } + } + else + { + NSIndexPath *indexPath = [self.recentsTableView indexPathForSelectedRow]; + if (indexPath) + { + [self.recentsTableView deselectRowAtIndexPath:indexPath animated:NO]; + } + } + } + else + { + [super refreshCurrentSelectedCell:forceVisible]; + } +} + +#pragma mark - + +- (void)scrollToNextRoomWithMissedNotifications +{ + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode == RecentsDataSourceModePeople) + { + [self scrollToTheTopTheNextRoomWithMissedNotificationsInSection:recentsDataSource.conversationSection]; + } +} + +#pragma mark - UISearchBarDelegate + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText +{ + // Apply filter on contact source + [contactsDataSource searchWithPattern:searchText forceReset:NO]; + + [super searchBar:searchBar textDidChange:searchText]; +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar +{ + // Reset filtering + [contactsDataSource searchWithPattern:nil forceReset:NO]; + + [super searchBarCancelButtonClicked:searchBar]; +} + +@end diff --git a/Riot/ViewController/RecentsViewController.h b/Riot/ViewController/RecentsViewController.h index edf3f0739..859b962b4 100644 --- a/Riot/ViewController/RecentsViewController.h +++ b/Riot/ViewController/RecentsViewController.h @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket Ltd + Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,9 +17,34 @@ #import -@class HomeViewController; +@interface RecentsViewController : MXKRecentListViewController +{ +@protected + /** + The image view of the (+) button. + */ + UIImageView* plusButtonImageView; + + /** + Current alert (if any). + */ + MXKAlert *currentAlert; + + /** + The list of the section headers currently displayed in the recents table. + */ + NSMutableArray *displayedSectionHeaders; + + /** + The current vertical position of the first displayed section header. + */ + CGFloat firstDisplayedSectionHeaderPosY; +} -@interface RecentsViewController : MXKRecentListViewController +@property (weak, nonatomic) IBOutlet UIView *stickyHeadersTopContainer; +@property (weak, nonatomic) IBOutlet UIView *stickyHeadersBottomContainer; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *stickyHeadersTopContainerHeightConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *stickyHeadersBottomContainerHeightConstraint; /** If YES, the table view will scroll at the top on the next data source refresh. @@ -27,21 +53,93 @@ @property (nonatomic) BOOL shouldScrollToTopOnRefresh; /** - Display the recents described in the provided data source. - - @param listDataSource the data source providing the recents list. - @param homeViewController the segmentedViewController in which the RecentsViewController is displayed. + Tell whether the search bar at the top of the recents table is enabled. YES by default. */ -- (void)displayList:(MXKRecentsDataSource*)listDataSource fromHomeViewController:(HomeViewController*)homeViewController; +@property (nonatomic) BOOL enableSearchBar; + +/** + Tell whether the drag and drop option are enabled. NO by default. + This option is used to move a room from a section to another. + */ +@property (nonatomic) BOOL enableDragging; + +/** + Tell whether the sticky headers are enabled. NO by default. + */ +@property (nonatomic) BOOL enableStickyHeaders; + +/** + Define the height of each sticky headers (30.0 by default). + */ +@property (nonatomic) CGFloat stickyHeaderHeight; + +/** + The Google Analytics Instance screen name (Default is "RecentsScreen"). + */ +@property (nonatomic) NSString *screenName; + +/** + Return the sticky header for the specified section of the table view + + @param tableView the table view object asking for the view object. + @param section an index number identifying a section of tableView . + @return the sticky header. + */ +- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section; + +/** + Release the resources used to display the sticky headers. + */ +- (void)resetStickyHeaders; + +/** + Prepare the sticky headers display. + */ +- (void)prepareStickyHeaders; /** Refresh the cell selection in the table. - This must be done accordingly to the currently selected room in the parent HomeViewController. + This must be done accordingly to the currently selected room in the master tabbar of the application. @param forceVisible if YES and if the corresponding cell is not visible, scroll the table view to make it visible. */ - (void)refreshCurrentSelectedCell:(BOOL)forceVisible; + +#pragma mark - Room handling +/** + Add the (+) button at the right bottom corner of the view. + */ +- (void)addPlusButton; + +/** + Action triggered when the user taps on the (+) button. + Create an empty room by default. + */ +- (void)onPlusButtonPressed; + +/** + Create an empty room. + */ +- (void)createAnEmptyRoom; + +/** + Join a room by alias or id. + */ +- (void)joinARoom; + +/** + Scroll the next room with missed notifications to the top. + + @param section the table section in which the operation must be applied. + */ +- (void)scrollToTheTopTheNextRoomWithMissedNotificationsInSection:(NSInteger)section; + +#pragma mark - Actions + +- (void)didTapOnSectionHeader:(UIGestureRecognizer*)gestureRecognizer; +- (void)didSwipeOnSectionHeader:(UISwipeGestureRecognizer*)gestureRecognizer; + @end diff --git a/Riot/ViewController/RecentsViewController.m b/Riot/ViewController/RecentsViewController.m index f03f3276b..363255c33 100644 --- a/Riot/ViewController/RecentsViewController.m +++ b/Riot/ViewController/RecentsViewController.m @@ -19,17 +19,14 @@ #import "RecentsDataSource.h" #import "RecentTableViewCell.h" -#import "RageShakeManager.h" +#import "UnifiedSearchViewController.h" #import "MXRoom+Riot.h" #import "NSBundle+MatrixKit.h" -#import "HomeViewController.h" #import "RoomViewController.h" -#import "RiotDesignValues.h" - #import "InviteRecentTableViewCell.h" #import "DirectoryRecentTableViewCell.h" #import "RoomIdOrAliasTableViewCell.h" @@ -38,16 +35,14 @@ @interface RecentsViewController () { - // The "parent" segmented view controller - HomeViewController *homeViewController; - // The room identifier related to the cell which is in editing mode (if any). NSString *editedRoomId; // Tell whether a recents refresh is pending (suspended during editing mode). BOOL isRefreshPending; - // recents drag and drop management + // Recents drag and drop management + UILongPressGestureRecognizer *longPressGestureRecognizer; UIImageView *cellSnapshot; NSIndexPath* movingCellPath; MXRoom* movingRoom; @@ -62,24 +57,34 @@ // Observe kMXNotificationCenterDidUpdateRules to update missed messages counts. id kMXNotificationCenterDidUpdateRulesObserver; + + MXHTTPOperation *currentRequest; + + // The fake search bar displayed at the top of the recents table. We switch on the actual search bar (self.recentsSearchBar) + // when the user selects it. + UISearchBar *tableSearchBar; } @end @implementation RecentsViewController -- (void)awakeFromNib +#pragma mark - Class methods + ++ (UINib *)nib { - [super awakeFromNib]; - - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) - { - self.preferredContentSize = CGSizeMake(320.0, 600.0); - } - - self.navigationItem.title = NSLocalizedStringFromTable(@"title_recents", @"Vector", nil); + return [UINib nibWithNibName:NSStringFromClass([RecentsViewController class]) + bundle:[NSBundle bundleForClass:[RecentsViewController class]]]; } ++ (instancetype)recentListViewController +{ + return [[[self class] alloc] initWithNibName:NSStringFromClass([RecentsViewController class]) + bundle:[NSBundle bundleForClass:[RecentsViewController class]]]; +} + +#pragma mark - + - (void)finalizeInit { [super finalizeInit]; @@ -88,6 +93,30 @@ self.defaultBarTintColor = kRiotNavBarTintColor; self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + // Set default screen name + _screenName = @"RecentsScreen"; + + // Enable the search bar in the recents table, and remove the search option from the navigation bar. + _enableSearchBar = YES; + self.enableBarButtonSearch = NO; + + _enableDragging = NO; + + _enableStickyHeaders = NO; + _stickyHeaderHeight = 30.0; + + // Create the fake search bar + tableSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 600, 44)]; + tableSearchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; + tableSearchBar.showsCancelButton = NO; + tableSearchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); + tableSearchBar.delegate = self; + + displayedSectionHeaders = [NSMutableArray array]; + + // Set itself as delegate by default. + self.delegate = self; } - (void)viewDidLoad @@ -100,15 +129,13 @@ // Register here the customized cell view class used to render recents [self.recentsTableView registerNib:RecentTableViewCell.nib forCellReuseIdentifier:RecentTableViewCell.defaultReuseIdentifier]; [self.recentsTableView registerNib:InviteRecentTableViewCell.nib forCellReuseIdentifier:InviteRecentTableViewCell.defaultReuseIdentifier]; - - UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onRecentsLongPress:)]; - [self.recentsTableView addGestureRecognizer:longPress]; - - self.recentsTableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; // Hide line separators of empty cells self.recentsTableView.tableFooterView = [[UIView alloc] init]; + // Apply dragging settings + self.enableDragging = _enableDragging; + // Observe UIApplicationDidEnterBackgroundNotification to refresh bubbles when app leaves the foreground state. UIApplicationDidEnterBackgroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -116,12 +143,29 @@ [self setEditing:NO]; }]; + + self.recentsSearchBar.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.recentsSearchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); } - (void)destroy { [super destroy]; + longPressGestureRecognizer = nil; + + if (currentRequest) + { + [currentRequest cancel]; + currentRequest = nil; + } + + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + if (UIApplicationDidEnterBackgroundNotificationObserver) { [[NSNotificationCenter defaultCenter] removeObserver:UIApplicationDidEnterBackgroundNotificationObserver]; @@ -146,48 +190,34 @@ { [super viewWillAppear:animated]; - // Check whether the view controller is displayed in its "parent" segmented view controller. - if (homeViewController) + // Screen tracking (via Google Analytics) + id tracker = [[GAI sharedInstance] defaultTracker]; + if (tracker) { - // Screen tracking (via Google Analytics) - id tracker = [[GAI sharedInstance] defaultTracker]; - if (tracker) - { - NSString *screenName = homeViewController.searchBarHidden ? @"RoomsList" : @"RoomsGlobalSearch"; - NSString *currentScreenName = [tracker get:kGAIScreenName]; - - if (!currentScreenName || ![currentScreenName isEqualToString:screenName]) - { - [tracker set:kGAIScreenName value:screenName]; - [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; - } - } - - // Deselect the current selected row, it will be restored on viewDidAppear (if any) - NSIndexPath *indexPath = [self.recentsTableView indexPathForSelectedRow]; - if (indexPath) - { - [self.recentsTableView deselectRowAtIndexPath:indexPath animated:NO]; - } - - // Observe kAppDelegateDidTapStatusBarNotificationObserver. - kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - - [self scrollToTop:YES]; - - }]; - - // Observe kMXNotificationCenterDidUpdateRules to refresh missed messages counts - kMXNotificationCenterDidUpdateRulesObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXNotificationCenterDidUpdateRules object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - - // Do not refresh if there is a pending recent drag and drop - if (!movingCellPath) - { - [self refreshRecentsTable]; - } - - }]; + [tracker set:kGAIScreenName value:_screenName]; + [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; } + + // Deselect the current selected row, it will be restored on viewDidAppear (if any) + NSIndexPath *indexPath = [self.recentsTableView indexPathForSelectedRow]; + if (indexPath) + { + [self.recentsTableView deselectRowAtIndexPath:indexPath animated:NO]; + } + + // Observe kAppDelegateDidTapStatusBarNotificationObserver. + kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + + [self scrollToTop:YES]; + + }]; + + // Observe kMXNotificationCenterDidUpdateRules to refresh missed messages counts + kMXNotificationCenterDidUpdateRulesObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXNotificationCenterDidUpdateRules object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + + [self refreshRecentsTable]; + + }]; } - (void)viewWillDisappear:(BOOL)animated @@ -214,11 +244,11 @@ { [super viewDidAppear:animated]; - // Release the current selected room (if any) except if the Room ViewController is still visible (see splitViewController.isCollapsed condition) - if (!self.splitViewController || self.splitViewController.isCollapsed) + // Release the current selected item (if any) except if the second view controller is still visible. + if (self.splitViewController.isCollapsed) { // Release the current selected room (if any). - [homeViewController closeSelectedRoom]; + [[AppDelegate theDelegate].masterTabBarController releaseSelectedItem]; } else { @@ -233,18 +263,30 @@ [super viewDidDisappear:animated]; } -#pragma mark - - -- (void)displayList:(MXKRecentsDataSource*)listDataSource fromHomeViewController:(HomeViewController*)homeViewController2 +- (void)viewDidLayoutSubviews { - [super displayList:listDataSource]; - homeViewController = homeViewController2; + [super viewDidLayoutSubviews]; + + dispatch_async(dispatch_get_main_queue(), ^{ + + [self refreshStickyHeadersContainersHeight]; + + }); } -#pragma mark - Internal methods +#pragma mark - Override MXKRecentListViewController - (void)refreshRecentsTable { + // Refresh the tabBar icon badges + [[AppDelegate theDelegate].masterTabBarController refreshTabBarBadges]; + + // do not refresh if there is a pending recent drag and drop + if (movingCellPath) + { + return; + } + if (editedRoomId) { // Check whether the user didn't leave the room @@ -263,52 +305,60 @@ isRefreshPending = NO; + // Force reset existing sticky headers if any + [self resetStickyHeaders]; + [self.recentsTableView reloadData]; + // Check conditions to display the fake search bar into the table header + if (_enableSearchBar && self.recentsSearchBar.isHidden && self.recentsTableView.tableHeaderView == nil) + { + // Add the search bar by hiding it by default. + self.recentsTableView.tableHeaderView = tableSearchBar; + self.recentsTableView.contentOffset = CGPointMake(0, self.recentsTableView.contentOffset.y + tableSearchBar.frame.size.height); + } + if (_shouldScrollToTopOnRefresh) { [self scrollToTop:NO]; _shouldScrollToTopOnRefresh = NO; } + [self prepareStickyHeaders]; + // In case of split view controller where the primary and secondary view controllers are displayed side-by-side on screen, // the selected room (if any) is updated and kept visible. - // Note: 'isCollapsed' property is available in UISplitViewController for iOS 8 and later. - if (self.splitViewController && (![self.splitViewController respondsToSelector:@selector(isCollapsed)] || !self.splitViewController.isCollapsed)) + if (!self.splitViewController.isCollapsed) { [self refreshCurrentSelectedCell:YES]; } +} + +- (void)hideSearchBar:(BOOL)hidden +{ + [super hideSearchBar:hidden]; - if (self.dataSource.mxSession.state == MXSessionStateRunning) + if (!hidden) { - // The Directory cell is displayed when the recents list is empty - RecentsDataSource *recentsDataSource = (RecentsDataSource*)self.dataSource; - if (recentsDataSource.hidePublicRoomsDirectory) - { - recentsDataSource.hidePublicRoomsDirectory = (self.recentsTableView.numberOfSections != 0); - } - else if (homeViewController.searchBarHidden) - { - recentsDataSource.hidePublicRoomsDirectory = (self.recentsTableView.numberOfSections > 1); - } + // Remove the fake table header view if any + self.recentsTableView.tableHeaderView = nil; + self.recentsTableView.contentInset = UIEdgeInsetsZero; } } -- (void)scrollToTop:(BOOL)animated -{ - [self.recentsTableView setContentOffset:CGPointMake(-self.recentsTableView.contentInset.left, -self.recentsTableView.contentInset.top) animated:animated]; -} +#pragma mark - - (void)refreshCurrentSelectedCell:(BOOL)forceVisible { // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. NSIndexPath *currentSelectedCellIndexPath = nil; - if (homeViewController.currentRoomViewController) + MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; + if (masterTabBarController.currentRoomViewController) { // Look for the rank of this selected room in displayed recents - currentSelectedCellIndexPath = [self.dataSource cellIndexPathWithRoomId:homeViewController.selectedRoomId andMatrixSession:homeViewController.selectedRoomSession]; + currentSelectedCellIndexPath = [self.dataSource cellIndexPathWithRoomId:masterTabBarController.selectedRoomId andMatrixSession:masterTabBarController.selectedRoomSession]; } - + if (currentSelectedCellIndexPath) { // Select the right row @@ -332,6 +382,323 @@ } } +#pragma mark - Sticky Headers + +- (void)setEnableStickyHeaders:(BOOL)enableStickyHeaders +{ + _enableStickyHeaders = enableStickyHeaders; + + // Refresh the table display if it is already rendered. + if (self.recentsTableView.contentSize.height) + { + [self refreshRecentsTable]; + } +} + +- (void)setStickyHeaderHeight:(CGFloat)stickyHeaderHeight +{ + if (_stickyHeaderHeight != stickyHeaderHeight) + { + _stickyHeaderHeight = stickyHeaderHeight; + + // Force a sticky headers refresh + self.enableStickyHeaders = _enableStickyHeaders; + } +} + +- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section +{ + // Return the section header by default. + return [self tableView:tableView viewForHeaderInSection:section]; +} + +- (void)resetStickyHeaders +{ + // Release sticky header + _stickyHeadersTopContainerHeightConstraint.constant = 0; + _stickyHeadersBottomContainerHeightConstraint.constant = 0; + + for (UIView *view in _stickyHeadersTopContainer.subviews) + { + [view removeFromSuperview]; + } + for (UIView *view in _stickyHeadersBottomContainer.subviews) + { + [view removeFromSuperview]; + } + + [displayedSectionHeaders removeAllObjects]; + + self.recentsTableView.contentInset = UIEdgeInsetsZero; +} + +- (void)prepareStickyHeaders +{ + // We suppose here [resetStickyHeaders] has been already called if need. + + NSInteger sectionsCount = self.recentsTableView.numberOfSections; + + if (self.enableStickyHeaders && sectionsCount) + { + NSUInteger topContainerOffset = 0; + NSUInteger bottomContainerOffset = 0; + CGRect frame; + + UIView *stickyHeader = [self viewForStickyHeaderInSection:0 withSwipeGestureRecognizerInDirection:UISwipeGestureRecognizerDirectionDown]; + frame = stickyHeader.frame; + frame.origin.y = topContainerOffset; + stickyHeader.frame = frame; + [self.stickyHeadersTopContainer addSubview:stickyHeader]; + topContainerOffset = stickyHeader.frame.size.height; + + for (NSUInteger index = 1; index < sectionsCount; index++) + { + stickyHeader = [self viewForStickyHeaderInSection:index withSwipeGestureRecognizerInDirection:UISwipeGestureRecognizerDirectionDown]; + frame = stickyHeader.frame; + frame.origin.y = topContainerOffset; + stickyHeader.frame = frame; + [self.stickyHeadersTopContainer addSubview:stickyHeader]; + topContainerOffset += frame.size.height; + + stickyHeader = [self viewForStickyHeaderInSection:index withSwipeGestureRecognizerInDirection:UISwipeGestureRecognizerDirectionUp]; + frame = stickyHeader.frame; + frame.origin.y = bottomContainerOffset; + stickyHeader.frame = frame; + [self.stickyHeadersBottomContainer addSubview:stickyHeader]; + bottomContainerOffset += frame.size.height; + } + + [self refreshStickyHeadersContainersHeight]; + } +} + +- (UIView *)viewForStickyHeaderInSection:(NSInteger)section withSwipeGestureRecognizerInDirection:(UISwipeGestureRecognizerDirection)swipeDirection +{ + UIView *stickyHeader = [self tableView:self.recentsTableView viewForStickyHeaderInSection:section]; + stickyHeader.tag = section; + stickyHeader.autoresizingMask = UIViewAutoresizingFlexibleWidth; + + // Remove existing gesture recognizers + while (stickyHeader.gestureRecognizers.count) + { + UIGestureRecognizer *gestureRecognizer = stickyHeader.gestureRecognizers.lastObject; + [stickyHeader removeGestureRecognizer:gestureRecognizer]; + } + + // Handle tap gesture, the section is moved up on the tap. + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnSectionHeader:)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [stickyHeader addGestureRecognizer:tap]; + + // Handle vertical swipe gesture with the provided direction, by default the section will be moved up on this swipe. + UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(didSwipeOnSectionHeader:)]; + [swipe setNumberOfTouchesRequired:1]; + [swipe setDirection:swipeDirection]; + [stickyHeader addGestureRecognizer:swipe]; + + return stickyHeader; +} + +- (void)didTapOnSectionHeader:(UIGestureRecognizer*)gestureRecognizer +{ + UIView *view = gestureRecognizer.view; + NSInteger section = view.tag; + + // Scroll to the top of this section + if ([self.recentsTableView numberOfRowsInSection:section] > 0) + { + [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + } +} + +- (void)didSwipeOnSectionHeader:(UISwipeGestureRecognizer*)gestureRecognizer +{ + UIView *view = gestureRecognizer.view; + NSInteger section = view.tag; + + if ([self.recentsTableView numberOfRowsInSection:section] > 0) + { + // Check whether the first cell of this section is already visible. + UITableViewCell *firstSectionCell = [self.recentsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; + if (firstSectionCell) + { + // Scroll to the top of the previous section (if any) + if (section && [self.recentsTableView numberOfRowsInSection:(section - 1)] > 0) + { + [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:(section - 1)] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + } + } + else + { + // Scroll to the top of this section + [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + } + } +} + +- (void)refreshStickyHeadersContainersHeight +{ + if (_enableStickyHeaders) + { + NSUInteger lowestSectionInBottomStickyHeader = NSNotFound; + CGFloat containerHeight; + + // Retrieve the first header actually visible in the recents table view. + // Caution: In some cases like the screen rotation, some displayed section headers are temporarily not visible. + UIView *firstDisplayedSectionHeader; + for (UIView *header in displayedSectionHeaders) + { + if (header.frame.origin.y + header.frame.size.height > self.recentsTableView.contentOffset.y) + { + firstDisplayedSectionHeader = header; + break; + } + } + + if (firstDisplayedSectionHeader) + { + // Initialize the top container height by considering the headers which are before the first visible section header. + containerHeight = 0; + for (UIView *header in _stickyHeadersTopContainer.subviews) + { + if (header.tag < firstDisplayedSectionHeader.tag) + { + containerHeight += self.stickyHeaderHeight; + } + } + + // Check whether the first visible section header is partially hidden. + if (firstDisplayedSectionHeader.frame.origin.y < self.recentsTableView.contentOffset.y) + { + // Compute the height of the hidden part. + CGFloat delta = self.recentsTableView.contentOffset.y - firstDisplayedSectionHeader.frame.origin.y; + + if (delta < self.stickyHeaderHeight) + { + containerHeight += delta; + } + else + { + containerHeight += self.stickyHeaderHeight; + } + } + + if (containerHeight) + { + self.stickyHeadersTopContainerHeightConstraint.constant = containerHeight; + self.recentsTableView.contentInset = UIEdgeInsetsMake(-self.stickyHeaderHeight, 0, 0, 0); + } + else + { + self.stickyHeadersTopContainerHeightConstraint.constant = 0; + self.recentsTableView.contentInset = UIEdgeInsetsZero; + } + + // Look for the lowest section index visible in the bottom sticky headers. + CGFloat maxVisiblePosY = self.recentsTableView.contentOffset.y + self.recentsTableView.frame.size.height - self.recentsTableView.contentInset.bottom; + UIView *lastDisplayedSectionHeader = displayedSectionHeaders.lastObject; + + for (UIView *header in _stickyHeadersBottomContainer.subviews) + { + if (header.tag > lastDisplayedSectionHeader.tag) + { + maxVisiblePosY -= self.stickyHeaderHeight; + } + } + + for (NSInteger index = displayedSectionHeaders.count; index > 0;) + { + lastDisplayedSectionHeader = displayedSectionHeaders[--index]; + if (lastDisplayedSectionHeader.frame.origin.y + self.stickyHeaderHeight > maxVisiblePosY) + { + maxVisiblePosY -= self.stickyHeaderHeight; + } + else + { + lowestSectionInBottomStickyHeader = lastDisplayedSectionHeader.tag + 1; + break; + } + } + } + else + { + // Handle here the case where no section header is currently displayed in the table. + // No more than one section is then displayed, we retrieve this section by checking the first visible cell. + NSIndexPath *firstCellIndexPath = [self.recentsTableView indexPathForRowAtPoint:CGPointMake(0, self.recentsTableView.contentOffset.y)]; + if (firstCellIndexPath) + { + NSInteger section = firstCellIndexPath.section; + + // Refresh top container of the sticky headers + CGFloat containerHeight = 0; + for (UIView *header in _stickyHeadersTopContainer.subviews) + { + if (header.tag <= section) + { + containerHeight += header.frame.size.height; + } + } + + self.stickyHeadersTopContainerHeightConstraint.constant = containerHeight; + if (containerHeight) + { + self.recentsTableView.contentInset = UIEdgeInsetsMake(-self.stickyHeaderHeight, 0, 0, 0); + } + else + { + self.recentsTableView.contentInset = UIEdgeInsetsZero; + } + + // Set the lowest section index visible in the bottom sticky headers. + lowestSectionInBottomStickyHeader = section + 1; + } + } + + // Update here the height of the bottom container of the sticky headers thanks to lowestSectionInBottomStickyHeader. + containerHeight = 0; + CGRect bounds = _stickyHeadersBottomContainer.frame; + bounds.origin.y = 0; + + for (UIView *header in _stickyHeadersBottomContainer.subviews) + { + if (header.tag > lowestSectionInBottomStickyHeader) + { + containerHeight += self.stickyHeaderHeight; + } + else if (header.tag == lowestSectionInBottomStickyHeader) + { + containerHeight += self.stickyHeaderHeight; + bounds.origin.y = header.frame.origin.y; + } + } + + if (self.stickyHeadersBottomContainerHeightConstraint.constant != containerHeight) + { + self.stickyHeadersBottomContainerHeightConstraint.constant = containerHeight; + self.stickyHeadersBottomContainer.bounds = bounds; + } + } +} + +#pragma mark - Internal methods + +- (void)scrollToTop:(BOOL)animated +{ + [self.recentsTableView setContentOffset:CGPointMake(-self.recentsTableView.contentInset.left, -self.recentsTableView.contentInset.top) animated:animated]; +} + +-(void)showPublicRoomsDirectory +{ + // Here the recents view controller is displayed inside a unified search view controller. + // Sanity check + if (self.parentViewController && [self.parentViewController isKindOfClass:UnifiedSearchViewController.class]) + { + // Show the directory screen + [((UnifiedSearchViewController*)self.parentViewController) showPublicRoomsDirectory]; + } +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -350,39 +717,26 @@ - (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData { - id cellDataStoring = (id )cellData; + Class class = [self cellViewClassForCellData:cellData]; - if (cellDataStoring.roomSummary.room.state.membership != MXMembershipInvite) + if ([class respondsToSelector:@selector(defaultReuseIdentifier)]) { - return RecentTableViewCell.defaultReuseIdentifier; - } - else - { - return InviteRecentTableViewCell.defaultReuseIdentifier; - } -} - -- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes -{ - // do not refresh if there is a pending recent drag and drop - if (movingCellPath) - { - return; + return [class defaultReuseIdentifier]; } - [self refreshRecentsTable]; + return nil; } - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo { - // Handle here user actions on recents for Vector app + // Handle here user actions on recents for Riot app if ([actionIdentifier isEqualToString:kInviteRecentTableViewCellPreviewButtonPressed]) { // Retrieve the invited room MXRoom *invitedRoom = userInfo[kInviteRecentTableViewCellRoomKey]; - // Display room preview by selecting it. - [self.delegate recentListViewController:self didSelectRoom:invitedRoom.state.roomId inMatrixSession:invitedRoom.mxSession]; + // Display the room preview + [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:invitedRoom.state.roomId andEventId:nil inMatrixSession:invitedRoom.mxSession]; } else if ([actionIdentifier isEqualToString:kInviteRecentTableViewCellDeclineButtonPressed]) { @@ -412,7 +766,7 @@ } } -#pragma mark - swipe actions +#pragma mark - Swipe actions - (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -707,7 +1061,27 @@ - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - return [(RecentsDataSource*)self.dataSource heightForHeaderInSection:section]; + return 30.0f; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + UIView *sectionHeader = [super tableView:tableView viewForHeaderInSection:section]; + sectionHeader.tag = section; + + while (sectionHeader.gestureRecognizers.count) + { + UIGestureRecognizer *gestureRecognizer = sectionHeader.gestureRecognizers.lastObject; + [sectionHeader removeGestureRecognizer:gestureRecognizer]; + } + + // Handle tap gesture + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnSectionHeader:)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [sectionHeader addGestureRecognizer:tap]; + + return sectionHeader; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath @@ -721,8 +1095,7 @@ } else if ([cell isKindOfClass:[DirectoryRecentTableViewCell class]]) { - // Show the directory screen - [homeViewController showPublicRoomsDirectory]; + [self showPublicRoomsDirectory]; } else if ([cell isKindOfClass:[RoomIdOrAliasTableViewCell class]]) { @@ -742,7 +1115,101 @@ } } -#pragma mark - recents drag & drop management +- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section +{ + if (_enableStickyHeaders) + { + view.tag = section; + + UIView *firstDisplayedSectionHeader = displayedSectionHeaders.firstObject; + + if (!firstDisplayedSectionHeader || section < firstDisplayedSectionHeader.tag) + { + [displayedSectionHeaders insertObject:view atIndex:0]; + } + else + { + [displayedSectionHeaders addObject:view]; + } + + [self refreshStickyHeadersContainersHeight]; + } +} + +- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section +{ + if (_enableStickyHeaders) + { + UIView *firstDisplayedSectionHeader = displayedSectionHeaders.firstObject; + if (firstDisplayedSectionHeader) + { + if (section == firstDisplayedSectionHeader.tag) + { + [displayedSectionHeaders removeObjectAtIndex:0]; + + [self refreshStickyHeadersContainersHeight]; + } + else + { + // This section header is the last displayed one. + // Add a sanity check in case of the header has been already removed. + UIView *lastDisplayedSectionHeader = displayedSectionHeaders.lastObject; + if (section == lastDisplayedSectionHeader.tag) + { + [displayedSectionHeaders removeLastObject]; + + [self refreshStickyHeadersContainersHeight]; + } + } + } + } +} + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + dispatch_async(dispatch_get_main_queue(), ^{ + + [self refreshStickyHeadersContainersHeight]; + + }); + + [super scrollViewDidScroll:scrollView]; + + if (scrollView == self.recentsTableView) + { + if (!self.recentsSearchBar.isHidden) + { + if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.contentInset.top > self.recentsSearchBar.frame.size.height)) + { + // Hide the search bar + [self hideSearchBar:YES]; + + // Refresh display + [self refreshRecentsTable]; + } + } + } +} + +#pragma mark - Recents drag & drop management + +- (void)setEnableDragging:(BOOL)enableDragging +{ + _enableDragging = enableDragging; + + if (_enableDragging && !longPressGestureRecognizer && self.recentsTableView) + { + longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onRecentsLongPress:)]; + [self.recentsTableView addGestureRecognizer:longPressGestureRecognizer]; + } + else if (longPressGestureRecognizer) + { + [self.recentsTableView removeGestureRecognizer:longPressGestureRecognizer]; + longPressGestureRecognizer = nil; + } +} - (void)onRecentsDragEnd { @@ -758,8 +1225,13 @@ [self.activityIndicator stopAnimating]; } -- (IBAction) onRecentsLongPress:(id)sender +- (IBAction)onRecentsLongPress:(id)sender { + if (sender != longPressGestureRecognizer) + { + return; + } + RecentsDataSource* recentsDataSource = nil; if ([self.dataSource isKindOfClass:[RecentsDataSource class]]) @@ -773,8 +1245,7 @@ return; } - UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender; - UIGestureRecognizerState state = longPress.state; + UIGestureRecognizerState state = longPressGestureRecognizer.state; // check if there is a moving cell during the long press managemnt if ((state != UIGestureRecognizerStateBegan) && !movingCellPath) @@ -782,7 +1253,7 @@ return; } - CGPoint location = [longPress locationInView:self.recentsTableView]; + CGPoint location = [longPressGestureRecognizer locationInView:self.recentsTableView]; switch (state) { @@ -951,5 +1422,317 @@ } } +#pragma mark - Room handling + +- (void)addPlusButton +{ + // Add room options button + plusButtonImageView = [[UIImageView alloc] init]; + [plusButtonImageView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.view addSubview:plusButtonImageView]; + + plusButtonImageView.backgroundColor = [UIColor clearColor]; + plusButtonImageView.contentMode = UIViewContentModeCenter; + plusButtonImageView.image = [UIImage imageNamed:@"create_room"]; + plusButtonImageView.layer.shadowOpacity = 0.3; + plusButtonImageView.layer.shadowOffset = CGSizeMake(0, 3); + + CGFloat side = 78.0f; + NSLayoutConstraint* widthConstraint = [NSLayoutConstraint constraintWithItem:plusButtonImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:side]; + + NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:plusButtonImageView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:side]; + + NSLayoutConstraint* trailingConstraint = [NSLayoutConstraint constraintWithItem:plusButtonImageView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:0]; + + NSLayoutConstraint* bottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:plusButtonImageView + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:9]; + + [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, trailingConstraint, bottomConstraint]]; + + plusButtonImageView.userInteractionEnabled = YES; + + // Handle tap gesture + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onPlusButtonPressed)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [plusButtonImageView addGestureRecognizer:tap]; +} + +- (void)onPlusButtonPressed +{ + __weak typeof(self) weakSelf = self; + + [currentAlert dismiss:NO]; + + currentAlert = [[MXKAlert alloc] initWithTitle:nil message:nil style:MXKAlertStyleActionSheet]; + + [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"room_recents_start_chat_with", @"Vector", nil) style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + [strongSelf performSegueWithIdentifier:@"presentStartChat" sender:strongSelf]; + }]; + + [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"room_recents_create_empty_room", @"Vector", nil) style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + [strongSelf createAnEmptyRoom]; + }]; + + [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"room_recents_join_room", @"Vector", nil) style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + [strongSelf joinARoom]; + }]; + + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:MXKAlertActionStyleCancel handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + }]; + + currentAlert.sourceView = plusButtonImageView; + + currentAlert.mxkAccessibilityIdentifier = @"RecentsVCCreateRoomAlert"; + [currentAlert showInViewController:self]; +} + +- (void)createAnEmptyRoom +{ + // Sanity check + if (self.mainSession) + { + // Create one room at time + if (!currentRequest) + { + [self startActivityIndicator]; + + // Create an empty room. + currentRequest = [self.mainSession createRoom:nil + visibility:kMXRoomDirectoryVisibilityPrivate + roomAlias:nil + topic:nil + success:^(MXRoom *room) { + + currentRequest = nil; + [self stopActivityIndicator]; + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:room.state.roomId andEventId:nil inMatrixSession:self.mainSession]; + + // Force the expanded header + [AppDelegate theDelegate].masterTabBarController.currentRoomViewController.showExpandedHeader = YES; + + } failure:^(NSError *error) { + + currentRequest = nil; + [self stopActivityIndicator]; + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + NSLog(@"[RecentsViewController] Create new room failed"); + + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + }]; + } + else + { + // Ask the user to wait + __weak __typeof(self) weakSelf = self; + currentAlert = [[MXKAlert alloc] initWithTitle:nil + message:NSLocalizedStringFromTable(@"room_creation_wait_for_creation", @"Vector", nil) + style:MXKAlertStyleAlert]; + + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] + style:MXKAlertActionStyleCancel + handler:^(MXKAlert *alert) { + + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf->currentAlert = nil; + + }]; + currentAlert.mxkAccessibilityIdentifier = @"RecentsVCRoomCreationInProgressAlert"; + [currentAlert showInViewController:self]; + } + } +} + +- (void)joinARoom +{ + [currentAlert dismiss:NO]; + + __weak typeof(self) weakSelf = self; + + // Prompt the user to type a room id or room alias + currentAlert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"room_recents_join_room_title", @"Vector", nil) + message:NSLocalizedStringFromTable(@"room_recents_join_room_prompt", @"Vector", nil) + style:MXKAlertStyleAlert]; + + [currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { + + textField.secureTextEntry = NO; + textField.placeholder = nil; + textField.keyboardType = UIKeyboardTypeDefault; + }]; + + currentAlert.cancelButtonIndex = [currentAlert addActionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + } + }]; + + [currentAlert addActionWithTitle:NSLocalizedStringFromTable(@"join", @"Vector", nil) style:MXKAlertActionStyleDefault handler:^(MXKAlert *alert) { + + if (weakSelf) + { + UITextField *textField = [alert textFieldAtIndex:0]; + NSString *roomAliasOrId = textField.text; + + typeof(self) self = weakSelf; + self->currentAlert = nil; + + [self.activityIndicator startAnimating]; + + self->currentRequest = [self.mainSession joinRoom:textField.text success:^(MXRoom *room) { + + self->currentRequest = nil; + [self.activityIndicator stopAnimating]; + + // Show the room + [[AppDelegate theDelegate] showRoom:room.state.roomId andEventId:nil withMatrixSession:self.mainSession]; + + } failure:^(NSError *error) { + + NSLog(@"[RecentsViewController] Join joinARoom (%@) failed", roomAliasOrId); + + self->currentRequest = nil; + [self.activityIndicator stopAnimating]; + + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + }]; + + currentAlert.mxkAccessibilityIdentifier = @"RecentsVCJoinARoomAlert"; + [currentAlert showInViewController:self]; +} + +#pragma mark - Table view scroll handling + +- (void)scrollToTheTopTheNextRoomWithMissedNotificationsInSection:(NSInteger)section +{ + UITableViewCell *firstVisibleCell = self.recentsTableView.visibleCells.firstObject; + if (firstVisibleCell) + { + NSIndexPath *firstVisibleCellIndexPath = [self.recentsTableView indexPathForCell:firstVisibleCell]; + NSInteger nextCellRow = (firstVisibleCellIndexPath.section == section) ? firstVisibleCellIndexPath.row + 1 : 0; + + // Look for the next room with missed notifications. + NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:nextCellRow inSection:section]; + nextCellRow++; + id cellData = [self.dataSource cellDataAtIndexPath:nextIndexPath]; + + while (cellData) + { + if (cellData.notificationCount) + { + [self.recentsTableView scrollToRowAtIndexPath:nextIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; + break; + } + nextIndexPath = [NSIndexPath indexPathForRow:nextCellRow inSection:section]; + nextCellRow++; + cellData = [self.dataSource cellDataAtIndexPath:nextIndexPath]; + } + + if (!cellData && [self.recentsTableView numberOfRowsInSection:section] > 0) + { + // Scroll back to the top. + [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + } + } +} + +#pragma mark - MXKRecentListViewControllerDelegate + +- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession +{ + // Open the room + [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:roomId andEventId:nil inMatrixSession:matrixSession]; +} + +#pragma mark - UISearchBarDelegate + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + [super scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; +} + +- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar +{ + if (searchBar == tableSearchBar) + { + [self hideSearchBar:NO]; + [self.recentsSearchBar becomeFirstResponder]; + return NO; + } + + return YES; + +} + +- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar +{ + dispatch_async(dispatch_get_main_queue(), ^{ + + [self.recentsSearchBar setShowsCancelButton:YES animated:NO]; + + }); +} + +- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar +{ + [self.recentsSearchBar setShowsCancelButton:NO animated:NO]; +} @end diff --git a/Riot/ViewController/RecentsViewController.xib b/Riot/ViewController/RecentsViewController.xib new file mode 100644 index 000000000..9eb164746 --- /dev/null +++ b/Riot/ViewController/RecentsViewController.xib @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/ViewController/RoomFilesSearchViewController.m b/Riot/ViewController/RoomFilesSearchViewController.m index 8aa2244e2..fa25e3a5b 100644 --- a/Riot/ViewController/RoomFilesSearchViewController.m +++ b/Riot/ViewController/RoomFilesSearchViewController.m @@ -24,10 +24,6 @@ #import "FilesSearchCellData.h" #import "FilesSearchTableViewCell.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import "AppDelegate.h" @interface RoomFilesSearchViewController () diff --git a/Riot/ViewController/RoomFilesViewController.m b/Riot/ViewController/RoomFilesViewController.m index 32d2c23ad..d602b906b 100644 --- a/Riot/ViewController/RoomFilesViewController.m +++ b/Riot/ViewController/RoomFilesViewController.m @@ -21,10 +21,6 @@ #import "AppDelegate.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import "AttachmentsViewController.h" @implementation RoomFilesViewController diff --git a/Riot/ViewController/RoomMemberDetailsViewController.m b/Riot/ViewController/RoomMemberDetailsViewController.m index 5f60d35fb..0db8f2965 100644 --- a/Riot/ViewController/RoomMemberDetailsViewController.m +++ b/Riot/ViewController/RoomMemberDetailsViewController.m @@ -21,10 +21,6 @@ #import "RoomMemberTitleView.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import "AvatarGenerator.h" #import "Tools.h" diff --git a/Riot/ViewController/RoomMessagesSearchViewController.m b/Riot/ViewController/RoomMessagesSearchViewController.m index 298ae5785..fd08d6521 100644 --- a/Riot/ViewController/RoomMessagesSearchViewController.m +++ b/Riot/ViewController/RoomMessagesSearchViewController.m @@ -26,10 +26,6 @@ #import "RoomIncomingAttachmentBubbleCell.h" #import "RoomIncomingTextMsgBubbleCell.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import "AppDelegate.h" @interface RoomMessagesSearchViewController () diff --git a/Riot/ViewController/RoomParticipantsViewController.m b/Riot/ViewController/RoomParticipantsViewController.m index 0f588565c..0fd839f48 100644 --- a/Riot/ViewController/RoomParticipantsViewController.m +++ b/Riot/ViewController/RoomParticipantsViewController.m @@ -143,7 +143,7 @@ [self.tableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"]; - // Add room creation button programatically + // Add room creation button programmatically [self addAddParticipantButton]; } @@ -508,7 +508,7 @@ - (void)addAddParticipantButton { - // Add blur mask programatically + // Add blur mask programmatically tableViewMaskLayer = [CAGradientLayer layer]; CGColorRef opaqueWhiteColor = [UIColor colorWithWhite:1.0 alpha:1.0].CGColor; @@ -592,30 +592,34 @@ // Set delegate to handle action on member (start chat, mention) contactsPickerViewController.contactsTableViewControllerDelegate = self; - contactsPickerViewController.forceMatrixIdInDisplayName = YES; + + // Prepare its data source + ContactsDataSource *contactsDataSource = [[ContactsDataSource alloc] init]; + contactsDataSource.areSectionsShrinkable = YES; + contactsDataSource.displaySearchInputInContactsList = YES; + contactsDataSource.forceMatrixIdInDisplayName = YES; // Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user. - contactsPickerViewController.contactCellAccessoryImage = [UIImage imageNamed:@"plus_icon"]; + contactsDataSource.contactCellAccessoryImage = [UIImage imageNamed:@"plus_icon"]; // List all the participants by their matrix user id, or a room 3pid invite token to ignore them during the contacts search. - [contactsPickerViewController.ignoredContactsByMatrixId removeAllObjects]; for (Contact *contact in actualParticipants) { - [contactsPickerViewController.ignoredContactsByMatrixId setObject:contact forKey:contact.mxMember.userId]; + [contactsDataSource.ignoredContactsByMatrixId setObject:contact forKey:contact.mxMember.userId]; } for (Contact *contact in invitedParticipants) { if (contact.mxMember) { - [contactsPickerViewController.ignoredContactsByMatrixId setObject:contact forKey:contact.mxMember.userId]; + [contactsDataSource.ignoredContactsByMatrixId setObject:contact forKey:contact.mxMember.userId]; } else if (contact.mxThirdPartyInvite) { - [contactsPickerViewController.ignoredContactsByMatrixId setObject:contact forKey:contact.mxThirdPartyInvite.token]; + [contactsDataSource.ignoredContactsByMatrixId setObject:contact forKey:contact.mxThirdPartyInvite.token]; } } if (userParticipant) { - [contactsPickerViewController.ignoredContactsByMatrixId setObject:userParticipant forKey:userParticipant.mxMember.userId]; + [contactsDataSource.ignoredContactsByMatrixId setObject:userParticipant forKey:userParticipant.mxMember.userId]; } [contactsPickerViewController showSearch:YES]; @@ -625,9 +629,11 @@ if (currentSearchText) { contactsPickerViewController.searchBar.text = currentSearchText; - [contactsPickerViewController searchWithPattern:currentSearchText forceReset:YES complete:nil]; + [contactsDataSource searchWithPattern:currentSearchText forceReset:YES]; } + [contactsPickerViewController displayList:contactsDataSource]; + [self pushViewController:contactsPickerViewController]; } @@ -1275,7 +1281,7 @@ currentAlert = nil; } - if (section == participantsSection && userParticipant && (0 == row)) + if (section == participantsSection && userParticipant && (0 == row) && !currentSearchText.length) { // Leave ? currentAlert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"room_participants_leave_prompt_title", @"Vector", nil) @@ -1323,16 +1329,30 @@ if (section == participantsSection) { - participants = actualParticipants; - - if (userParticipant) + if (currentSearchText.length) { - row --; + participants = filteredActualParticipants; + } + else + { + participants = actualParticipants; + + if (userParticipant) + { + row --; + } } } else { - participants = invitedParticipants; + if (currentSearchText.length) + { + participants = filteredInvitedParticipants; + } + else + { + participants = invitedParticipants; + } } if (row < participants.count) diff --git a/Riot/ViewController/RoomSearchViewController.m b/Riot/ViewController/RoomSearchViewController.m index 1892c29c3..bdbb2e2fa 100644 --- a/Riot/ViewController/RoomSearchViewController.m +++ b/Riot/ViewController/RoomSearchViewController.m @@ -285,9 +285,12 @@ RoomViewController *roomViewController = segue.destinationViewController; RoomDataSource *roomDataSource = [[RoomDataSource alloc] initWithRoomId:selectedSearchEvent.roomId initialEventId:selectedSearchEvent.eventId andMatrixSession:selectedSearchEventSession]; [roomDataSource finalizeInitialization]; + roomDataSource.markTimelineInitialEvent = YES; [roomViewController displayRoom:roomDataSource]; roomViewController.hasRoomDataSourceOwnership = YES; + + roomViewController.navigationItem.leftItemsSupplementBackButton = YES; } // Hide back button title diff --git a/Riot/ViewController/RoomSettingsViewController.m b/Riot/ViewController/RoomSettingsViewController.m index 8408a7c0b..abccffe11 100644 --- a/Riot/ViewController/RoomSettingsViewController.m +++ b/Riot/ViewController/RoomSettingsViewController.m @@ -22,10 +22,6 @@ #import "SegmentedViewController.h" -#import "RageShakeManager.h" - -#import "RiotDesignValues.h" - #import "AvatarGenerator.h" #import "Tools.h" diff --git a/Riot/ViewController/RoomViewController.h b/Riot/ViewController/RoomViewController.h index dbdb39542..45fd0f0bd 100644 --- a/Riot/ViewController/RoomViewController.h +++ b/Riot/ViewController/RoomViewController.h @@ -35,6 +35,13 @@ @property (weak, nonatomic) IBOutlet UIView *previewHeaderContainer; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; +// The jump to last unread banner +@property (weak, nonatomic) IBOutlet UIView *jumpToLastUnreadBannerContainer; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *jumpToLastUnreadBannerContainerTopConstraint; +@property (weak, nonatomic) IBOutlet UIButton *jumpToLastUnreadButton; +@property (weak, nonatomic) IBOutlet UILabel *jumpToLastUnreadLabel; +@property (weak, nonatomic) IBOutlet UIButton *resetReadMarkerButton; + /** Force the display of the expanded header. The default value is NO: this expanded header is hidden on new instantiated RoomViewController object. @@ -57,5 +64,10 @@ */ - (void)displayRoomPreview:(RoomPreviewData*)roomPreviewData; +/** + Action used to handle some buttons. + */ +- (IBAction)onButtonPressed:(id)sender; + @end diff --git a/Riot/ViewController/RoomViewController.m b/Riot/ViewController/RoomViewController.m index 7229ceeda..47a74d510 100644 --- a/Riot/ViewController/RoomViewController.m +++ b/Riot/ViewController/RoomViewController.m @@ -18,13 +18,10 @@ #import "RoomViewController.h" #import "RoomDataSource.h" +#import "RoomBubbleCellData.h" #import "AppDelegate.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" - #import "RoomInputToolbarView.h" #import "RoomActivitiesView.h" @@ -48,6 +45,8 @@ #import "UsersDevicesViewController.h" +#import "RoomEmptyBubbleCell.h" + #import "RoomIncomingTextMsgBubbleCell.h" #import "RoomIncomingTextMsgWithoutSenderInfoBubbleCell.h" #import "RoomIncomingTextMsgWithPaginationTitleBubbleCell.h" @@ -89,13 +88,13 @@ #import "AvatarGenerator.h" #import "Tools.h" -#import "RiotDesignValues.h" - #import "GBDeviceInfo_iOS.h" #import "RoomEncryptedDataBubbleCell.h" #import "EncryptionInfoView.h" +#import "MXRoom+Riot.h" + @interface RoomViewController () { // The expanded header @@ -157,6 +156,12 @@ // Observer kMXRoomSummaryDidChangeNotification to keep updated the missed discussion count id mxRoomSummaryDidChangeObserver; + + // The table view cell in which the read marker is displayed (nil by default). + MXKRoomBubbleTableViewCell *readMarkerTableViewCell; + + // Tell whether the view controller is appeared or not. + BOOL isAppeared; } @end @@ -266,6 +271,11 @@ [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:RoomEmptyBubbleCell.class forCellReuseIdentifier:RoomEmptyBubbleCell.defaultReuseIdentifier]; + + // Prepare jump to last unread banner + self.jumpToLastUnreadLabel.attributedText = [[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"room_jump_to_first_unread", @"Vector", nil) attributes:@{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), NSUnderlineColorAttributeName: kRiotTextColorBlack, NSForegroundColorAttributeName: kRiotTextColorBlack}]; + // Prepare expanded header self.expandedHeaderContainer.backgroundColor = kRiotColorLightGrey; @@ -451,12 +461,20 @@ } [self removeCallNotificationsListeners]; + + // Re-enable the read marker display, and disable its update. + self.roomDataSource.showReadMarker = YES; + self.updateRoomReadMarker = NO; + isAppeared = NO; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; + isAppeared = YES; + [self checkReadMarkerVisibility]; + if (self.roomDataSource) { // Set visible room id @@ -470,6 +488,7 @@ }]; [self refreshActivitiesViewDisplay]; + [self refreshJumpToLastUnreadBannerDisplay]; // Observe missed notifications mxRoomSummaryDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomSummaryDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -594,6 +613,7 @@ self.expandedHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height; self.bubblesTableViewTopConstraint.constant = self.expandedHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top; + self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.expandedHeaderContainerHeightConstraint.constant; } // Check whether the preview header is visible else if (previewHeader) @@ -620,6 +640,11 @@ self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height; self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top; + self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant; + } + else + { + self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.bubblesTableView.contentInset.top; } [self refreshMissedDiscussionsCount:YES]; @@ -694,6 +719,15 @@ #pragma mark - Override MXKRoomViewController +- (void)onMatrixSessionChange +{ + [super onMatrixSessionChange]; + + // Re-enable the read marker display, and disable its update. + self.roomDataSource.showReadMarker = YES; + self.updateRoomReadMarker = NO; +} + - (void)displayRoom:(MXKRoomDataSource *)dataSource { // Remove potential preview Data @@ -703,12 +737,18 @@ [self removeMatrixSession:self.mainSession]; } + // Enable the read marker display, and disable its update. + dataSource.showReadMarker = YES; + self.updateRoomReadMarker = NO; + [super displayRoom:dataSource]; customizedRoomDataSource = nil; if (self.roomDataSource) { + self.eventsAcknowledgementEnabled = YES; + // Set room title view [self refreshRoomTitle]; @@ -896,6 +936,12 @@ }); } + + // Make the activity indicator follow the keyboard + // At runtime, this creates a smooth animation + CGPoint activityIndicatorCenter = self.activityIndicator.center; + activityIndicatorCenter.y = self.view.center.y - keyboardHeight / 2; + self.activityIndicator.center = activityIndicatorCenter; } - (void)dismissTemporarySubViews @@ -909,6 +955,19 @@ } } +- (void)setBubbleTableViewDisplayInTransition:(BOOL)bubbleTableViewDisplayInTransition +{ + if (self.isBubbleTableViewDisplayInTransition != bubbleTableViewDisplayInTransition) + { + [super setBubbleTableViewDisplayInTransition:bubbleTableViewDisplayInTransition]; + + [self refreshActivitiesViewDisplay]; + + [self checkReadMarkerVisibility]; + [self refreshJumpToLastUnreadBannerDisplay]; + } +} + - (void)destroy { self.navigationItem.rightBarButtonItem.enabled = NO; @@ -1193,6 +1252,7 @@ animations:^{ self.bubblesTableViewTopConstraint.constant = (isVisible ? self.expandedHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top : 0); + self.jumpToLastUnreadBannerContainerTopConstraint.constant = (isVisible ? self.expandedHeaderContainerHeightConstraint.constant : self.bubblesTableView.contentInset.top); if (roomAvatarView) { @@ -1307,6 +1367,7 @@ animations:^{ self.bubblesTableViewTopConstraint.constant = 0; + self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.bubblesTableView.contentInset.top; // Force to render the view [self forceLayoutRefresh]; @@ -1398,6 +1459,7 @@ animations:^{ self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top; + self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant; if (roomAvatarView) { @@ -1422,6 +1484,8 @@ if (previewData) { + self.eventsAcknowledgementEnabled = NO; + [self addMatrixSession:previewData.mxSession]; roomPreviewData = previewData; @@ -1447,8 +1511,12 @@ { id bubbleData = (id)cellData; - // Select the suitable table view cell class - if (bubbleData.isIncoming) + // Select the suitable table view cell class, by considering first the empty bubble cell. + if (!bubbleData.attributedTextMessage) + { + cellViewClass = RoomEmptyBubbleCell.class; + } + else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) { @@ -1544,13 +1612,6 @@ #pragma mark - MXKDataSource delegate -- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes -{ - [super dataSource:dataSource didCellChange:changes]; - - [self refreshActivitiesViewDisplay]; -} - - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo { // Handle here user actions on bubbles for Vector app @@ -2394,15 +2455,13 @@ completion (finished); } - // Here the placeholder may have been defined temporarily to display IRC command usage. - // The original placeholder (savedInputToolbarPlaceholder) will be restored during the handling of the next typing notification + // Consider here the saved placeholder only if no new placeholder has been defined during the height animation. if (!toolbarView.placeholder) { // Restore the placeholder if any toolbarView.placeholder = savedInputToolbarPlaceholder.length ? savedInputToolbarPlaceholder : nil; - savedInputToolbarPlaceholder = nil; } - + savedInputToolbarPlaceholder = nil; }]; } } @@ -2434,10 +2493,61 @@ { [self performSegueWithIdentifier:@"showRoomSearch" sender:self]; } + else if (sender == self.jumpToLastUnreadButton) + { + // Hide expanded header to restore navigation bar settings. + [self showExpandedHeader:NO]; + // Dismiss potential keyboard. + [self dismissKeyboard]; + + MXKRoomDataSource *roomDataSource; + // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. + roomDataSource = [[RoomDataSource alloc] initWithRoomId:self.roomDataSource.roomId initialEventId:self.roomDataSource.room.accountData.readMarkerEventId andMatrixSession:self.mainSession]; + [roomDataSource finalizeInitialization]; + + // Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view. + self.centerBubblesTableViewContentOnTheInitialEventBottom = YES; + [self displayRoom:roomDataSource]; + + // Give the data source ownership to the room view controller. + self.hasRoomDataSourceOwnership = YES; + } + else if (sender == self.resetReadMarkerButton) + { + // Move the read marker to the current read receipt position. + [self.roomDataSource.room forgetReadMarker]; + + // Hide the banner + self.jumpToLastUnreadBannerContainer.hidden = YES; + } } #pragma mark - UITableViewDelegate +- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + { + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; + if (roomBubbleTableViewCell.readMarkerView) + { + readMarkerTableViewCell = roomBubbleTableViewCell; + + [self checkReadMarkerVisibility]; + } + } +} + +- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath +{ + if (cell == readMarkerTableViewCell) + { + readMarkerTableViewCell = nil; + } + + [super tableView:tableView didEndDisplayingCell:cell forRowAtIndexPath:indexPath]; +} + - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [super tableView:tableView didSelectRowAtIndexPath:indexPath]; @@ -2445,6 +2555,13 @@ #pragma mark - +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + [super scrollViewDidScroll:scrollView]; + + [self checkReadMarkerVisibility]; +} + - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewWillBeginDragging:)]) @@ -2480,6 +2597,7 @@ [self onScrollViewDidEndScrolling:scrollView]; [self refreshActivitiesViewDisplay]; + [self refreshJumpToLastUnreadBannerDisplay]; } else { @@ -2501,6 +2619,7 @@ } [self refreshActivitiesViewDisplay]; + [self refreshJumpToLastUnreadBannerDisplay]; } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView @@ -2511,6 +2630,7 @@ } [self refreshActivitiesViewDisplay]; + [self refreshJumpToLastUnreadBannerDisplay]; } - (void)onScrollViewDidEndScrolling:(UIScrollView *)scrollView @@ -2627,10 +2747,11 @@ { RoomDataSource *roomDataSource = [[RoomDataSource alloc] initWithRoomId:self.roomDataSource.roomId initialEventId:eventId andMatrixSession:self.mainSession]; [roomDataSource finalizeInitialization]; - - self.hasRoomDataSourceOwnership = YES; + roomDataSource.markTimelineInitialEvent = YES; [self displayRoom:roomDataSource]; + + self.hasRoomDataSourceOwnership = YES; } else { @@ -2902,8 +3023,9 @@ } else if ([self checkUnsentMessages] == NO) { - // Show "scroll to bottom" icon when the most recent message is not visible - if ([self isBubblesTableScrollViewAtTheBottom] == NO) + // Show "scroll to bottom" icon when the most recent message is not visible, + // or when the timelime is not live (this icon is used to go back to live). + if (!self.roomDataSource.isLive || (!self.bubblesTableView.isHidden && [self isBubblesTableScrollViewAtTheBottom] == NO)) { // Retrieve the unread messages count NSUInteger unreadCount = self.roomDataSource.room.summary.localUnreadEventCount; @@ -2917,7 +3039,37 @@ [roomActivitiesView displayScrollToBottomIcon:unreadCount onIconTapGesture:^{ - [self scrollBubblesTableViewToBottomAnimated:YES]; + if (self.roomDataSource.isLive) + { + // Enable the read marker display, and disable its update (in order to not mark as read all the new messages by default). + self.roomDataSource.showReadMarker = YES; + self.updateRoomReadMarker = NO; + + [self scrollBubblesTableViewToBottomAnimated:YES]; + } + else + { + // Switch back to the room live timeline managed by MXKRoomDataSourceManager + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession]; + MXKRoomDataSource *roomDataSource = [roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId create:YES]; + + // Scroll to bottom the bubble history on the display refresh. + shouldScrollToBottomOnTableRefresh = YES; + + [self displayRoom:roomDataSource]; + + // The room view controller do not have here the data source ownership. + self.hasRoomDataSourceOwnership = NO; + + [self refreshActivitiesViewDisplay]; + [self refreshJumpToLastUnreadBannerDisplay]; + + if (self.saveProgressTextInput) + { + // Restore the potential message partially typed before jump to last unread messages. + self.inputToolbarView.textMessage = roomDataSource.partialTextMessage; + } + } }]; } @@ -2946,17 +3098,26 @@ } NSUInteger highlightCount = 0; - NSUInteger missedCount = [self.mainSession missedDiscussionsCount]; - if (missedCount && self.roomDataSource.room.summary.notificationCount) + NSUInteger missedCount = [[AppDelegate theDelegate].masterTabBarController missedDiscussionsCount]; + + // Compute the missed notifications count of the current room by considering its notification mode in Riot. + NSUInteger roomNotificationCount = self.roomDataSource.room.summary.notificationCount; + if (self.roomDataSource.room.isMentionsOnly) + { + // Only the highlighted missed messages must be considered here. + roomNotificationCount = self.roomDataSource.room.summary.highlightCount; + } + + // Remove the current room from the missed discussion counter. + if (missedCount && roomNotificationCount) { - // Remove the current room from the missed discussion counter missedCount--; } if (missedCount) { // Compute the missed highlight count - highlightCount = [self.mainSession missedHighlightDiscussionsCount]; + highlightCount = [[AppDelegate theDelegate].masterTabBarController missedHighlightDiscussionsCount]; if (highlightCount && self.roomDataSource.room.summary.highlightCount) { // Remove the current room from the missed highlight counter @@ -3314,5 +3475,136 @@ [self.view setNeedsUpdateConstraints]; } + + +#pragma mark - Read marker handling + +- (void)checkReadMarkerVisibility +{ + if (readMarkerTableViewCell && isAppeared && !self.isBubbleTableViewDisplayInTransition) + { + // Check whether the read marker is visible + CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.contentInset.top; + CGFloat readMarkerViewPosY = readMarkerTableViewCell.frame.origin.y + readMarkerTableViewCell.readMarkerView.frame.origin.y; + if (contentTopPosY <= readMarkerViewPosY) + { + // Compute the max vertical position visible according to contentOffset + CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.contentInset.bottom; + if (readMarkerViewPosY <= contentBottomPosY) + { + // Launch animation + [self animateReadMarkerView]; + + // Disable the read marker display when it has been rendered once. + self.roomDataSource.showReadMarker = NO; + [self refreshJumpToLastUnreadBannerDisplay]; + + // Update the read marker position according the events acknowledgement in this view controller. + self.updateRoomReadMarker = YES; + + if (self.roomDataSource.isLive) + { + // Move the read marker to the current read receipt position. + [self.roomDataSource.room forgetReadMarker]; + } + } + } + } +} + +- (void)animateReadMarkerView +{ + // Check whether the cell with the read marker is known and if the marker is not animated yet. + if (readMarkerTableViewCell && readMarkerTableViewCell.readMarkerView.isHidden) + { + RoomBubbleCellData *cellData = (RoomBubbleCellData*)readMarkerTableViewCell.bubbleData; + + // Do not display the marker if this is the last message. + if (cellData.containsLastMessage && readMarkerTableViewCell.readMarkerView.tag == cellData.mostRecentComponentIndex) + { + readMarkerTableViewCell.readMarkerView.hidden = YES; + readMarkerTableViewCell = nil; + } + else + { + readMarkerTableViewCell.readMarkerView.hidden = NO; + + // Animate the layout to hide the read marker + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + + [UIView animateWithDuration:1.5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn + animations:^{ + + readMarkerTableViewCell.readMarkerViewLeadingConstraint.constant = readMarkerTableViewCell.readMarkerViewTrailingConstraint.constant = readMarkerTableViewCell.bubbleOverlayContainer.frame.size.width / 2; + readMarkerTableViewCell.readMarkerView.alpha = 0; + + // Force to render the view + [readMarkerTableViewCell.bubbleOverlayContainer layoutIfNeeded]; + + } + completion:^(BOOL finished){ + + readMarkerTableViewCell.readMarkerView.hidden = YES; + readMarkerTableViewCell.readMarkerView.alpha = 1; + + readMarkerTableViewCell = nil; + }]; + + }); + } + } +} + +- (void)refreshJumpToLastUnreadBannerDisplay +{ + // This banner is only displayed when the room timeline is in live (and no peeking). + // Check whether the read marker exists and has not been rendered yet. + if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId) + { + UITableViewCell *cell = [self.bubblesTableView visibleCells].firstObject; + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + { + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; + // Check whether the read marker is inside the first displayed cell. + if (roomBubbleTableViewCell.readMarkerView) + { + // The read marker display is still enabled (see roomDataSource.showReadMarker flag), + // this means the read marker was not been visible yet. + // We show the banner if the marker is located in the top hidden part of the cell. + CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.contentInset.top; + CGFloat readMarkerViewPosY = roomBubbleTableViewCell.frame.origin.y + roomBubbleTableViewCell.readMarkerView.frame.origin.y; + self.jumpToLastUnreadBannerContainer.hidden = (contentTopPosY < readMarkerViewPosY); + } + else + { + // Check whether the read marker event is anterior to the first event displayed in the first rendered cell. + MXKRoomBubbleComponent *component = roomBubbleTableViewCell.bubbleData.bubbleComponents.firstObject; + MXEvent *firstDisplayedEvent = component.event; + MXEvent *currentReadMarkerEvent = [self.roomDataSource.mxSession.store eventWithEventId:self.roomDataSource.room.accountData.readMarkerEventId inRoom:self.roomDataSource.roomId]; + + if (!currentReadMarkerEvent || (currentReadMarkerEvent.originServerTs < firstDisplayedEvent.originServerTs)) + { + self.jumpToLastUnreadBannerContainer.hidden = NO; + } + else + { + self.jumpToLastUnreadBannerContainer.hidden = YES; + } + } + } + } + else + { + self.jumpToLastUnreadBannerContainer.hidden = YES; + + // Initialize the read marker if it does not exist yet, only in case of live timeline. + if (!self.roomDataSource.room.accountData.readMarkerEventId && self.roomDataSource.isLive && !self.roomDataSource.isPeeking) + { + // Move the read marker to the current read receipt position by default. + [self.roomDataSource.room forgetReadMarker]; + } + } +} + @end diff --git a/Riot/ViewController/RoomViewController.xib b/Riot/ViewController/RoomViewController.xib index 75f7afe66..794153e2e 100644 --- a/Riot/ViewController/RoomViewController.xib +++ b/Riot/ViewController/RoomViewController.xib @@ -1,11 +1,12 @@ - + - + + @@ -16,8 +17,13 @@ + + + + + @@ -54,6 +60,76 @@ + @@ -75,6 +151,7 @@ + @@ -84,14 +161,20 @@ + + + + + + diff --git a/Riot/ViewController/RoomsViewController.h b/Riot/ViewController/RoomsViewController.h new file mode 100644 index 000000000..cf5f40859 --- /dev/null +++ b/Riot/ViewController/RoomsViewController.h @@ -0,0 +1,30 @@ +/* + Copyright 2017 Vector Creations 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" + +/** + The `RoomsViewController` screen is the view controller displayed when `Rooms` tab is selected. + */ +@interface RoomsViewController : RecentsViewController + +/** + Scroll the next room with missed notifications to the top. + */ +- (void)scrollToNextRoomWithMissedNotifications; + + +@end diff --git a/Riot/ViewController/RoomsViewController.m b/Riot/ViewController/RoomsViewController.m new file mode 100644 index 000000000..53bb95b3d --- /dev/null +++ b/Riot/ViewController/RoomsViewController.m @@ -0,0 +1,322 @@ +/* + Copyright 2017 Vector Creations 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 "RoomsViewController.h" + +#import "AppDelegate.h" + +#import "RecentsDataSource.h" + +#import "DirectoryServerPickerViewController.h" + +@interface RoomsViewController () +{ + RecentsDataSource *recentsDataSource; + + // The animated view displayed at the table view bottom when paginating the room directory + UIView* footerSpinnerView; +} +@end + +@implementation RoomsViewController + +- (void)finalizeInit +{ + [super finalizeInit]; + + self.screenName = @"Rooms"; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.accessibilityIdentifier = @"RoomsVCView"; + self.recentsTableView.accessibilityIdentifier = @"RoomsVCTableView"; + + // Tag the recents table with the its recents data source mode. + // This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods). + self.recentsTableView.tag = RecentsDataSourceModeRooms; + + // Add the (+) button programmatically + [self addPlusButton]; + + self.enableStickyHeaders = YES; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_rooms", @"Vector", nil); + + if ([self.dataSource isKindOfClass:RecentsDataSource.class]) + { + BOOL isFirstTime = (recentsDataSource != self.dataSource); + + // Take the lead on the shared data source. + recentsDataSource = (RecentsDataSource*)self.dataSource; + recentsDataSource.areSectionsShrinkable = NO; + [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeRooms]; + + if (isFirstTime) + { + // The first time the screen is displayed, make publicRoomsDirectoryDataSource + // start loading data + [recentsDataSource.publicRoomsDirectoryDataSource paginate:nil failure:nil]; + } + } +} + +- (void)dealloc +{ + +} + +- (void)destroy +{ + [super destroy]; +} + +#pragma mark - Override RecentsViewController + +- (void)refreshCurrentSelectedCell:(BOOL)forceVisible +{ + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode != RecentsDataSourceModeRooms) + { + return; + } + + [super refreshCurrentSelectedCell:forceVisible]; +} + +- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section +{ + CGRect frame = [tableView rectForHeaderInSection:section]; + frame.size.height = self.stickyHeaderHeight; + + return [recentsDataSource viewForHeaderInSection:section withFrame:frame]; +} + +- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo +{ + if ([actionIdentifier isEqualToString:kRecentsDataSourceTapOnDirectoryServerChange]) + { + // Show the directory server picker + [self performSegueWithIdentifier:@"presentDirectoryServerPicker" sender:self]; + } + else + { + [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo]; + } +} + +#pragma mark - + +- (void)scrollToNextRoomWithMissedNotifications +{ + // Check whether the recents data source is correctly configured. + if (recentsDataSource.recentsDataSourceMode == RecentsDataSourceModeRooms) + { + [self scrollToTheTopTheNextRoomWithMissedNotificationsInSection:recentsDataSource.conversationSection]; + } +} + +#pragma mark - Navigation + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender +{ + [super prepareForSegue:segue sender:sender]; + + UIViewController *pushedViewController = [segue destinationViewController]; + + if ([[segue identifier] isEqualToString:@"presentDirectoryServerPicker"]) + { + UINavigationController *pushedNavigationViewController = (UINavigationController*)pushedViewController; + DirectoryServerPickerViewController* directoryServerPickerViewController = (DirectoryServerPickerViewController*)pushedNavigationViewController.viewControllers.firstObject; + + MXKDirectoryServersDataSource *directoryServersDataSource = [[MXKDirectoryServersDataSource alloc] initWithMatrixSession:recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; + [directoryServersDataSource finalizeInitialization]; + + // Add directory servers from the app settings plist + NSArray *roomDirectoryServers = [[NSUserDefaults standardUserDefaults] objectForKey:@"roomDirectoryServers"]; + directoryServersDataSource.roomDirectoryServers = roomDirectoryServers; + + [directoryServerPickerViewController displayWithDataSource:directoryServersDataSource onComplete:^(id cellData) { + if (cellData) + { + // Use the selected directory server + if (cellData.thirdPartyProtocolInstance) + { + recentsDataSource.publicRoomsDirectoryDataSource.thirdpartyProtocolInstance = cellData.thirdPartyProtocolInstance; + } + else if (cellData.homeserver) + { + recentsDataSource.publicRoomsDirectoryDataSource.includeAllNetworks = cellData.includeAllNetworks; + recentsDataSource.publicRoomsDirectoryDataSource.homeserver = cellData.homeserver; + } + + // Refresh data + [self addSpinnerFooterView]; + + [recentsDataSource.publicRoomsDirectoryDataSource paginate:^(NSUInteger roomsAdded) { + + // The table view is automatically filled + [self removeSpinnerFooterView]; + + // Make the directory section appear full-page + [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:recentsDataSource.directorySection] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + + } failure:^(NSError *error) { + + [self removeSpinnerFooterView]; + }]; + } + }]; + + // Hide back button title + pushedViewController.navigationController.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; + } +} + +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + if (section == recentsDataSource.directorySection) + { + // Let the recents dataSource provide the height of this section header + return [recentsDataSource heightForHeaderInSection:section]; + } + + return [super tableView:tableView heightForHeaderInSection:section]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == recentsDataSource.directorySection) + { + [self openPublicRoomAtIndexPath:indexPath]; + } + else + { + [super tableView:tableView didSelectRowAtIndexPath:indexPath]; + } +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + // Trigger inconspicuous pagination on directy when user scrolls down + if ((scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.size.height) < 300) + { + [self triggerDirectoryPagination]; + } + + [super scrollViewDidScroll:scrollView]; +} + +#pragma mark - Private methods + +- (void)openPublicRoomAtIndexPath:(NSIndexPath *)indexPath +{ + MXPublicRoom *publicRoom = [recentsDataSource.publicRoomsDirectoryDataSource roomAtIndexPath:indexPath]; + + // Check whether the user has already joined the selected public room + if ([recentsDataSource.publicRoomsDirectoryDataSource.mxSession roomWithRoomId:publicRoom.roomId]) + { + // Open the public room + [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:publicRoom.roomId andEventId:nil inMatrixSession:recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; + } + else + { + // Preview the public room + if (publicRoom.worldReadable) + { + RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:publicRoom.roomId andSession:recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; + + [self startActivityIndicator]; + + // Try to get more information about the room before opening its preview + [roomPreviewData peekInRoom:^(BOOL succeeded) { + + [self stopActivityIndicator]; + + [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + }]; + } + else + { + RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; + [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + } + } +} + +- (void)triggerDirectoryPagination +{ + if (recentsDataSource.publicRoomsDirectoryDataSource.hasReachedPaginationEnd || footerSpinnerView) + { + // We got all public rooms or we are already paginating + // Do nothing + return; + } + + [self addSpinnerFooterView]; + + [recentsDataSource.publicRoomsDirectoryDataSource paginate:^(NSUInteger roomsAdded) { + + // The table view is automatically filled + [self removeSpinnerFooterView]; + + } failure:^(NSError *error) { + + [self removeSpinnerFooterView]; + }]; +} + +- (void)addSpinnerFooterView +{ + if (!footerSpinnerView) + { + UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f); + CGRect frame = spinner.frame; + frame.size.height = 80; // 80 * 0.75 = 60 + spinner.bounds = frame; + + spinner.color = [UIColor darkGrayColor]; + spinner.hidesWhenStopped = NO; + spinner.backgroundColor = [UIColor clearColor]; + [spinner startAnimating]; + + // No need to manage constraints here, iOS defines them + self.recentsTableView.tableFooterView = footerSpinnerView = spinner; + } +} + +- (void)removeSpinnerFooterView +{ + if (footerSpinnerView) + { + footerSpinnerView = nil; + + // Hide line separators of empty cells + self.recentsTableView.tableFooterView = [[UIView alloc] init];; + } +} + +@end diff --git a/Riot/ViewController/SegmentedViewController.m b/Riot/ViewController/SegmentedViewController.m index 97e79c474..f203a094b 100644 --- a/Riot/ViewController/SegmentedViewController.m +++ b/Riot/ViewController/SegmentedViewController.m @@ -17,9 +17,7 @@ #import "SegmentedViewController.h" -#import "RiotDesignValues.h" - -#import "RageShakeManager.h" +#import "AppDelegate.h" @interface SegmentedViewController () { diff --git a/Riot/ViewController/SettingsViewController.m b/Riot/ViewController/SettingsViewController.m index 9524af54c..8003a2d95 100644 --- a/Riot/ViewController/SettingsViewController.m +++ b/Riot/ViewController/SettingsViewController.m @@ -25,6 +25,7 @@ #import "AppDelegate.h" #import "AvatarGenerator.h" +#import "BugReportViewController.h" #import "CountryPickerViewController.h" #import "MXKEncryptionKeysExportView.h" #import "NBPhoneNumberUtil.h" @@ -54,6 +55,8 @@ enum { NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0, NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX, + NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX, + NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX, //NOTIFICATION_SETTINGS_CONTAINING_MY_USER_NAME_INDEX, //NOTIFICATION_SETTINGS_CONTAINING_MY_DISPLAY_NAME_INDEX, //NOTIFICATION_SETTINGS_SENT_TO_ME_INDEX, @@ -81,6 +84,7 @@ enum OTHER_CRASH_REPORT_INDEX, OTHER_MARK_ALL_AS_READ_INDEX, OTHER_CLEAR_CACHE_INDEX, + OTHER_REPORT_BUG_INDEX, OTHER_COUNT }; @@ -1524,6 +1528,30 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); cell = globalInfoCell; } + else if (row == NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_pin_rooms_with_missed_notif", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = [[NSUserDefaults standardUserDefaults] boolForKey:@"pinRoomsWithMissedNotif"]; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(togglePinRoomsWithMissedNotif:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + else if (row == NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_pin_rooms_with_unread", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = [[NSUserDefaults standardUserDefaults] boolForKey:@"pinRoomsWithUnread"]; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(togglePinRoomsWithUnread:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } } else if (section == SETTINGS_SECTION_CALLS_INDEX) { @@ -1727,6 +1755,26 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); cell = clearCacheBtnCell; } + else if (row == OTHER_REPORT_BUG_INDEX) + { + MXKTableViewCellWithButton *reportBugBtnCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]]; + if (!reportBugBtnCell) + { + reportBugBtnCell = [[MXKTableViewCellWithButton alloc] init]; + } + + NSString *btnTitle = NSLocalizedStringFromTable(@"settings_report_bug", @"Vector", nil); + [reportBugBtnCell.mxkButton setTitle:btnTitle forState:UIControlStateNormal]; + [reportBugBtnCell.mxkButton setTitle:btnTitle forState:UIControlStateHighlighted]; + [reportBugBtnCell.mxkButton setTintColor:kRiotColorGreen]; + reportBugBtnCell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; + + [reportBugBtnCell.mxkButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [reportBugBtnCell.mxkButton addTarget:self action:@selector(reportBug:) forControlEvents:UIControlEventTouchUpInside]; + reportBugBtnCell.mxkButton.accessibilityIdentifier = nil; + + cell = reportBugBtnCell; + } } else if (section == SETTINGS_SECTION_LABS_INDEX) { @@ -2158,6 +2206,7 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); #pragma mark - actions + - (void)onSignout:(id)sender { [currentAlert dismiss:NO]; @@ -2524,6 +2573,22 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); [self.tableView reloadData]; } +- (void)togglePinRoomsWithMissedNotif:(id)sender +{ + UISwitch *switchButton = (UISwitch*)sender; + + [[NSUserDefaults standardUserDefaults] setBool:switchButton.on forKey:@"pinRoomsWithMissedNotif"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)togglePinRoomsWithUnread:(id)sender +{ + UISwitch *switchButton = (UISwitch*)sender; + + [[NSUserDefaults standardUserDefaults] setBool:switchButton.on forKey:@"pinRoomsWithUnread"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + - (void)markAllAsRead:(id)sender { // Feedback: disable button and run activity indicator @@ -2555,6 +2620,12 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); }); } +- (void)reportBug:(id)sender +{ + BugReportViewController *bugReportViewController = [BugReportViewController bugReportViewController]; + [bugReportViewController showInViewController:self]; +} + - (void)selectPhoneNumberCountry:(id)sender { newPhoneNumberCountryPicker = [CountryPickerViewController countryPickerViewController]; @@ -2603,7 +2674,7 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(); // } //} -// + - (void)onSave:(id)sender { // sanity check diff --git a/Riot/ViewController/StartChatViewController.h b/Riot/ViewController/StartChatViewController.h index 4fbfe617f..11b4f3ba6 100644 --- a/Riot/ViewController/StartChatViewController.h +++ b/Riot/ViewController/StartChatViewController.h @@ -19,7 +19,7 @@ /** 'StartChatViewController' instance is used to prepare new room creation. */ -@interface StartChatViewController : ContactsTableViewController +@interface StartChatViewController : ContactsTableViewController @property (weak, nonatomic) IBOutlet UIView *searchBarHeader; @property (weak, nonatomic) IBOutlet UISearchBar *searchBarView; diff --git a/Riot/ViewController/StartChatViewController.m b/Riot/ViewController/StartChatViewController.m index 6cbe16b98..47f82c85d 100644 --- a/Riot/ViewController/StartChatViewController.m +++ b/Riot/ViewController/StartChatViewController.m @@ -21,6 +21,9 @@ @interface StartChatViewController () { + // The contact used to describe the current user. + MXKContact *userContact; + // Section indexes NSInteger participantsSection; @@ -62,7 +65,7 @@ { [super finalizeInit]; - self.forceMatrixIdInDisplayName = YES; + self.screenName = @"StartChat"; _isAddParticipantSearchBarEditing = NO; @@ -72,9 +75,16 @@ // Assign itself as delegate self.contactsTableViewControllerDelegate = self; + // Prepare its data source + ContactsDataSource *dataSource = [[ContactsDataSource alloc] init]; + dataSource.areSectionsShrinkable = YES; + dataSource.displaySearchInputInContactsList = YES; + dataSource.forceMatrixIdInDisplayName = YES; // Add a plus icon to the contact cell when a search session is in progress, // in order to make it more understandable for the end user. - self.contactCellAccessoryImage = [UIImage imageNamed:@"plus_icon"];; + dataSource.contactCellAccessoryImage = [UIImage imageNamed:@"plus_icon"]; + + [self displayList:dataSource]; } - (void)viewDidLoad @@ -82,13 +92,6 @@ [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. - // Check whether the view controller has been pushed via storyboard - if (!self.tableView) - { - // Instantiate view controller objects - [[[self class] nib] instantiateWithOwner:self options:nil]; - } - // Adjust Top and Bottom constraints to take into account potential navBar and tabBar. [NSLayoutConstraint deactivateConstraints:@[_searchBarTopConstraint, _tableViewBottomConstraint]]; @@ -103,7 +106,7 @@ _tableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual - toItem:self.tableView + toItem:self.contactsTableView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f]; @@ -133,9 +136,12 @@ _searchBarHeaderBorder.backgroundColor = kRiotColorSilver; // Hide line separators of empty cells - self.tableView.tableFooterView = [[UIView alloc] init]; + self.contactsTableView.tableFooterView = [[UIView alloc] init]; - [self.tableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"]; + [self.contactsTableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"]; + + // Redirect table data source + self.contactsTableView.dataSource = self; } - (void)destroy @@ -160,6 +166,10 @@ { [super addMatrixSession:mxSession]; + // FIXME: Handle multi accounts + NSString *displayName = NSLocalizedStringFromTable(@"you", @"Vector", nil); + userContact = [[MXKContact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:self.mainSession.myUser.userId]; + [self refreshParticipants]; } @@ -167,14 +177,6 @@ { [super viewWillAppear:animated]; - // Screen tracking (via Google Analytics) - id tracker = [[GAI sharedInstance] defaultTracker]; - if (tracker) - { - [tracker set:kGAIScreenName value:@"StartChat"]; - [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; - } - // Active the search session if the current participant list is empty if (!participants.count) { @@ -183,7 +185,7 @@ else { // Refresh display - [self refreshTableView]; + [self refreshContactsTable]; } } @@ -215,7 +217,7 @@ _isAddParticipantSearchBarEditing = isAddParticipantSearchBarEditing; // Switch the display between search result and participants list - [self refreshTableView]; + [self refreshContactsTable]; } } @@ -225,6 +227,8 @@ { // Refer all participants in ignored contacts dictionary. isMultiUseNameByDisplayName = [NSMutableDictionary dictionary]; + [contactsDataSource.ignoredContactsByMatrixId removeAllObjects]; + [contactsDataSource.ignoredContactsByEmail removeAllObjects]; for (MXKContact* contact in participants) { @@ -232,7 +236,7 @@ if (identifiers.count) { // Here the contact can only have one identifier - [self.ignoredContactsByMatrixId setObject:contact forKey:identifiers.firstObject]; + [contactsDataSource.ignoredContactsByMatrixId setObject:contact forKey:identifiers.firstObject]; } else { @@ -241,7 +245,7 @@ { // Here the contact can only have one email MXKEmail *email = emails.firstObject; - [self.ignoredContactsByEmail setObject:contact forKey:email.emailAddress]; + [contactsDataSource.ignoredContactsByEmail setObject:contact forKey:email.emailAddress]; } } isMultiUseNameByDisplayName[contact.displayName] = (isMultiUseNameByDisplayName[contact.displayName] ? @(YES) : @(NO)); @@ -249,7 +253,7 @@ if (userContact) { - [self.ignoredContactsByMatrixId setObject:userContact forKey:self.mainSession.myUser.userId]; + [contactsDataSource.ignoredContactsByMatrixId setObject:userContact forKey:self.mainSession.myUser.userId]; } } @@ -262,11 +266,10 @@ if (_isAddParticipantSearchBarEditing) { participantsSection = -1; - count = [super numberOfSectionsInTableView:self.tableView]; + count = [contactsDataSource numberOfSectionsInTableView:tableView]; } else { - searchInputSection = filteredLocalContactsSection = filteredMatrixContactsSection = -1; participantsSection = count++; } @@ -279,7 +282,7 @@ if (_isAddParticipantSearchBarEditing) { - count = [super tableView:self.tableView numberOfRowsInSection:section]; + count = [contactsDataSource tableView:tableView numberOfRowsInSection:section]; } else if (section == participantsSection) { @@ -295,7 +298,7 @@ if (_isAddParticipantSearchBarEditing) { - cell = [super tableView:self.tableView cellForRowAtIndexPath:indexPath]; + cell = [contactsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; } else if (indexPath.section == participantsSection) { @@ -355,12 +358,22 @@ if (_isAddParticipantSearchBarEditing) { - height = [super tableView:self.tableView heightForHeaderInSection:section]; + height = [contactsDataSource heightForHeaderInSection:section]; } return height; } +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (_isAddParticipantSearchBarEditing) + { + return [super tableView:tableView heightForRowAtIndexPath:indexPath]; + } + + return 74; +} + - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (_isAddParticipantSearchBarEditing) @@ -410,7 +423,7 @@ [self refreshParticipants]; - [self refreshTableView]; + [self refreshContactsTable]; } } @@ -587,53 +600,24 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { - [self searchWithPattern:searchText forceReset:NO complete:nil]; + [contactsDataSource searchWithPattern:searchText forceReset:NO]; } - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar -{ - // Check whether the access to the local contacts has not been already asked. - if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) - { - // Allow by default the local contacts sync in order to discover matrix users. - // This setting change will trigger the loading of the local contacts, which will automatically - // ask user permission to access their local contacts. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - +{ self.isAddParticipantSearchBarEditing = YES; searchBar.showsCancelButton = NO; return YES; } -- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar -{ - // "Done" key has been pressed. - - // Check whether the current search input is a valid email or a Matrix user ID - if (currentSearchText.length && ([MXTools isEmailAddress:currentSearchText] || [MXTools isMatrixUserIdentifier:currentSearchText])) - { - // Select the contact related to the search input, rather than having to hit + - if (searchInputSection != -1) - { - [self tableView:self.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:searchInputSection]]; - return; - } - - } - - // Dismiss keyboard - [_searchBarView resignFirstResponder]; -} - - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { searchBar.text = nil; self.isAddParticipantSearchBarEditing = NO; // Reset filtering - [self searchWithPattern:nil forceReset:NO complete:nil]; + [contactsDataSource searchWithPattern:nil forceReset:NO]; // Leave search [searchBar resignFirstResponder]; diff --git a/Riot/ViewController/StartChatViewController.xib b/Riot/ViewController/StartChatViewController.xib index 8d459ce93..c7710dc19 100644 --- a/Riot/ViewController/StartChatViewController.xib +++ b/Riot/ViewController/StartChatViewController.xib @@ -1,5 +1,5 @@ - - + + @@ -11,11 +11,11 @@ + - diff --git a/Riot/ViewController/UnifiedSearchViewController.h b/Riot/ViewController/UnifiedSearchViewController.h new file mode 100644 index 000000000..c4ccca308 --- /dev/null +++ b/Riot/ViewController/UnifiedSearchViewController.h @@ -0,0 +1,40 @@ +/* + Copyright 2017 Vector Creations 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 "SegmentedViewController.h" + +#import "ContactsTableViewController.h" + +/** + The `UnifiedSearchViewController` screen is the global search screen. + */ +@interface UnifiedSearchViewController : SegmentedViewController + +/** + Open the public rooms directory page. + It uses the `publicRoomsDirectoryDataSource` managed by the recents view controller data source + */ +- (void)showPublicRoomsDirectory; + +/** + Tell whether an event has been selected from messages or files search tab. + */ +@property (nonatomic, readonly) MXEvent *selectedSearchEvent; +@property (nonatomic, readonly) MXSession *selectedSearchEventSession; + +@end diff --git a/Riot/ViewController/UnifiedSearchViewController.m b/Riot/ViewController/UnifiedSearchViewController.m new file mode 100644 index 000000000..df3b1066b --- /dev/null +++ b/Riot/ViewController/UnifiedSearchViewController.m @@ -0,0 +1,536 @@ +/* + Copyright 2017 Vector Creations 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 "UnifiedSearchViewController.h" + +#import "UnifiedSearchRecentsDataSource.h" +#import "RecentsViewController.h" + +#import "RoomDataSource.h" +#import "RoomViewController.h" + +#import "DirectoryViewController.h" +#import "ContactDetailsViewController.h" +#import "SettingsViewController.h" + +#import "HomeMessagesSearchViewController.h" +#import "HomeMessagesSearchDataSource.h" +#import "HomeFilesSearchViewController.h" +#import "FilesSearchCellData.h" + +#import "AppDelegate.h" + +#import "GBDeviceInfo_iOS.h" + +@interface UnifiedSearchViewController () +{ + RecentsViewController *recentsViewController; + UnifiedSearchRecentsDataSource *recentsDataSource; + + HomeMessagesSearchViewController *messagesSearchViewController; + HomeMessagesSearchDataSource *messagesSearchDataSource; + + HomeFilesSearchViewController *filesSearchViewController; + MXKSearchDataSource *filesSearchDataSource; + + ContactsTableViewController *peopleSearchViewController; + ContactsDataSource *peopleSearchDataSource; + + // Current alert (if any). + MXKAlert *currentAlert; +} + +@end + +@implementation UnifiedSearchViewController + +- (void)finalizeInit +{ + [super finalizeInit]; + + // The navigation bar tint color and the rageShake Manager are handled by super (see SegmentedViewController). +} + +- (void)viewDidLoad +{ + // Set up the SegmentedVC tabs before calling [super viewDidLoad] + NSMutableArray* viewControllers = [[NSMutableArray alloc] init]; + NSMutableArray* titles = [[NSMutableArray alloc] init]; + + [titles addObject: NSLocalizedStringFromTable(@"search_rooms", @"Vector", nil)]; + recentsViewController = [RecentsViewController recentListViewController]; + recentsViewController.enableSearchBar = NO; + recentsViewController.screenName = @"UnifiedSearchRooms"; + [viewControllers addObject:recentsViewController]; + + [titles addObject: NSLocalizedStringFromTable(@"search_messages", @"Vector", nil)]; + messagesSearchViewController = [HomeMessagesSearchViewController searchViewController]; + [viewControllers addObject:messagesSearchViewController]; + + // Add search People tab + [titles addObject: NSLocalizedStringFromTable(@"search_people", @"Vector", nil)]; + peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; + peopleSearchViewController.contactsTableViewControllerDelegate = self; + [viewControllers addObject:peopleSearchViewController]; + + // add Files tab + [titles addObject: NSLocalizedStringFromTable(@"search_files", @"Vector", nil)]; + filesSearchViewController = [HomeFilesSearchViewController searchViewController]; + [viewControllers addObject:filesSearchViewController]; + + [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; + + [super viewDidLoad]; + + // Add the Vector background image when search bar is empty + [self addBackgroundImageViewToView:self.view]; + + // Initialize here the data sources if a matrix session has been already set. + [self initializeDataSources]; + + self.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); + + [super showSearch:NO]; +} + +- (void)destroy +{ + [super destroy]; + + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + self.searchBar.delegate = nil; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + // Screen tracking (via Google Analytics) + id tracker = [[GAI sharedInstance] defaultTracker]; + if (tracker) + { + [tracker set:kGAIScreenName value:@"UnifiedSearch"]; + [tracker send:[[GAIDictionaryBuilder createScreenView] build]]; + } + + // Let's child display the loading not the home view controller + if (self.activityIndicator) + { + [self.activityIndicator stopAnimating]; + self.activityIndicator = nil; + } + + // Reset searches + [recentsDataSource searchWithPatterns:nil]; + + [self updateSearch]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + // Here the actual view size is available, check the background image display if any + [self checkAndShowBackgroundImage]; + + if (self.splitViewController && self.splitViewController.isCollapsed) + { + // In case of split view controller where the primary and secondary view controllers are displayed side-by-side onscreen, + // the selected room (if any) is highlighted. + [self refreshCurrentSelectedCellInChild:YES]; + } +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + [self checkAndShowBackgroundImage]; +} + +#pragma mark - + +- (MXEvent*)selectedSearchEvent +{ + if (messagesSearchViewController.selectedEvent) + { + return messagesSearchViewController.selectedEvent; + } + return filesSearchViewController.selectedEvent; +} + +- (MXSession*)selectedSearchEventSession +{ + if (messagesSearchViewController.selectedEvent) + { + return messagesSearchDataSource.mxSession; + } + return filesSearchDataSource.mxSession; +} + +#pragma mark - + +- (void)initializeDataSources +{ + MXSession *mainSession = self.mainSession; + + if (mainSession) + { + // Init the recents data source + recentsDataSource = [[UnifiedSearchRecentsDataSource alloc] initWithMatrixSession:mainSession]; + [recentsViewController displayList:recentsDataSource]; + + // Init the search for messages + messagesSearchDataSource = [[HomeMessagesSearchDataSource alloc] initWithMatrixSession:mainSession]; + [messagesSearchViewController displaySearch:messagesSearchDataSource]; + + // Init the search for messages + filesSearchDataSource = [[MXKSearchDataSource alloc] initWithMatrixSession:mainSession]; + filesSearchDataSource.roomEventFilter.containsURL = YES; + filesSearchDataSource.shouldShowRoomDisplayName = YES; + [filesSearchDataSource registerCellDataClass:FilesSearchCellData.class forCellIdentifier:kMXKSearchCellDataIdentifier]; + [filesSearchViewController displaySearch:filesSearchDataSource]; + + // Init the search for people + peopleSearchDataSource = [[ContactsDataSource alloc] init]; + peopleSearchDataSource.areSectionsShrinkable = YES; + peopleSearchDataSource.displaySearchInputInContactsList = YES; + peopleSearchDataSource.contactCellAccessoryType = UITableViewCellAccessoryDisclosureIndicator; + [peopleSearchViewController displayList:peopleSearchDataSource]; + + // Check whether there are others sessions + NSArray* mxSessions = self.mxSessions; + if (mxSessions.count > 1) + { + for (MXSession *mxSession in mxSessions) + { + if (mxSession != mainSession) + { + // Add the session to the recents data source + [recentsDataSource addMatrixSession:mxSession]; + + // FIXME: Update messagesSearchDataSource and filesSearchDataSource + } + } + } + } +} + +- (void)addMatrixSession:(MXSession *)mxSession +{ + // Check whether the controller'€™s view is loaded into memory. + if (recentsViewController) + { + // Check whether the data sources have been initialized. + if (!recentsDataSource) + { + // Add first the session. The updated sessions list will be used during data sources initialization. + [super addMatrixSession:mxSession]; + + // Prepare data sources and return + [self initializeDataSources]; + return; + } + else + { + // Add the session to the existing recents data source + [recentsDataSource addMatrixSession:mxSession]; + + // FIXME: Update messagesSearchDataSource and filesSearchDataSource + } + } + + [super addMatrixSession:mxSession]; +} + +- (void)removeMatrixSession:(MXSession *)mxSession +{ + [recentsDataSource removeMatrixSession:mxSession]; + + // Check whether there are others sessions + if (!recentsDataSource.mxSessions.count) + { + [recentsViewController displayList:nil]; + [recentsDataSource destroy]; + recentsDataSource = nil; + } + + // FIXME: Handle correctly messagesSearchDataSource and filesSearchDataSource + + [super removeMatrixSession:mxSession]; +} + +- (void)showPublicRoomsDirectory +{ + // Force hiding the keyboard + [self.searchBar resignFirstResponder]; + + [self performSegueWithIdentifier:@"showDirectory" sender:self]; +} + +#pragma mark - Override MXKViewController + +- (void)setKeyboardHeight:(CGFloat)keyboardHeight +{ + [self setKeyboardHeightForBackgroundImage:keyboardHeight]; + + [super setKeyboardHeight:keyboardHeight]; + + [self checkAndShowBackgroundImage]; +} + +- (void)startActivityIndicator +{ + // Redirect the operation to the currently displayed VC + // It is a MXKViewController or a MXKTableViewController. So it supports startActivityIndicator + [self.selectedViewController performSelector:@selector(startActivityIndicator)]; +} + +- (void)stopActivityIndicator +{ + // The selected view controller mwy have changed since the call of [self startActivityIndicator] + // So, stop the activity indicator for all children + for (UIViewController *viewController in self.viewControllers) + { + [viewController performSelector:@selector(stopActivityIndicator)]; + } + } + +#pragma mark - Override UIViewController+VectorSearch + +- (void)setKeyboardHeightForBackgroundImage:(CGFloat)keyboardHeight +{ + [super setKeyboardHeightForBackgroundImage:keyboardHeight]; + + if (keyboardHeight > 0) + { + [self checkAndShowBackgroundImage]; + } +} + +// Check conditions before displaying the background +- (void)checkAndShowBackgroundImage +{ + // Note: This background is hidden when keyboard is dismissed. + // The other conditions depend on the current selected view controller. + if (self.selectedViewController == recentsViewController) + { + self.backgroundImageView.hidden = YES; + } + else if (self.selectedViewController == messagesSearchViewController) + { + self.backgroundImageView.hidden = ((messagesSearchDataSource.serverCount != 0) || !messagesSearchViewController.noResultsLabel.isHidden || (self.keyboardHeight == 0)); + } + else if (self.selectedViewController == peopleSearchViewController) + { + self.backgroundImageView.hidden = (([peopleSearchViewController.contactsTableView numberOfSections] != 0) || (self.keyboardHeight == 0)); + } + else if (self.selectedViewController == filesSearchViewController) + { + self.backgroundImageView.hidden = ((filesSearchDataSource.serverCount != 0) || !filesSearchViewController.noResultsLabel.isHidden || (self.keyboardHeight == 0)); + } + else + { + self.backgroundImageView.hidden = (self.keyboardHeight == 0); + } + + if (!self.backgroundImageView.hidden) + { + [self.backgroundImageView layoutIfNeeded]; + [self.selectedViewController.view layoutIfNeeded]; + + // Check whether there is enough space to display this background + // For example, in landscape with the iPhone 5 & 6 screen size, the backgroundImageView must be hidden. + if (self.backgroundImageView.frame.origin.y < 0 || (self.selectedViewController.view.frame.size.height - self.backgroundImageViewBottomConstraint.constant) < self.backgroundImageView.frame.size.height) + { + self.backgroundImageView.hidden = YES; + } + } +} + +#pragma mark - Override SegmentedViewController + +- (void)setSelectedIndex:(NSUInteger)selectedIndex +{ + [super setSelectedIndex:selectedIndex]; + + if (self.selectedViewController == peopleSearchViewController) + { + self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_people_placeholder", @"Vector", nil); + } + else + { + self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil); + } + + [self updateSearch]; +} + +#pragma mark - Internal methods + +// Made the currently displayed child update its selected cell +- (void)refreshCurrentSelectedCellInChild:(BOOL)forceVisible +{ + // TODO: Manage other children than recents + [recentsViewController refreshCurrentSelectedCell:forceVisible]; + + [peopleSearchViewController refreshCurrentSelectedCell:forceVisible]; +} + +#pragma mark - Navigation + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender +{ + // Keep ref on destinationViewController + [super prepareForSegue:segue sender:sender]; + + if ([[segue identifier] isEqualToString:@"showDirectory"]) + { + DirectoryViewController *directoryViewController = segue.destinationViewController; + [directoryViewController displayWitDataSource:recentsDataSource.publicRoomsDirectoryDataSource]; + } + + // Hide back button title + self.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; +} + +#pragma mark - Search + +- (void)hideSearch:(BOOL)animated +{ + [self withdrawViewControllerAnimated:animated completion:nil]; +} + +// Update search results under the currently selected tab +- (void)updateSearch +{ + if (self.searchBar.text.length) + { + recentsDataSource.hideRecents = NO; + self.backgroundImageView.hidden = YES; + + // Forward the search request to the data source + if (self.selectedViewController == recentsViewController) + { + // Do a AND search on words separated by a space + NSArray *patterns = [self.searchBar.text componentsSeparatedByString:@" "]; + + [recentsDataSource searchWithPatterns:patterns]; + recentsViewController.shouldScrollToTopOnRefresh = YES; + } + else if (self.selectedViewController == messagesSearchViewController) + { + // Launch the search only if the keyboard is no more visible + if (!self.searchBar.isFirstResponder) + { + // Do it asynchronously to give time to messagesSearchViewController to be set up + // so that it can display its loading wheel + dispatch_async(dispatch_get_main_queue(), ^{ + [messagesSearchDataSource searchMessages:self.searchBar.text force:NO]; + messagesSearchViewController.shouldScrollToBottomOnRefresh = YES; + }); + } + } + else if (self.selectedViewController == peopleSearchViewController) + { + [peopleSearchDataSource searchWithPattern:self.searchBar.text forceReset:NO]; + } + else if (self.selectedViewController == filesSearchViewController) + { + // Launch the search only if the keyboard is no more visible + if (!self.searchBar.isFirstResponder) + { + // Do it asynchronously to give time to filesSearchViewController to be set up + // so that it can display its loading wheel + dispatch_async(dispatch_get_main_queue(), ^{ + [filesSearchDataSource searchMessages:self.searchBar.text force:NO]; + filesSearchViewController.shouldScrollToBottomOnRefresh = YES; + }); + } + } + } + else + { + // Nothing to search, show only the public dictionary + recentsDataSource.hideRecents = YES; + + // Reset search result (if any) + [recentsDataSource searchWithPatterns:nil]; + if (messagesSearchDataSource.searchText.length) + { + [messagesSearchDataSource searchMessages:nil force:NO]; + } + + [peopleSearchDataSource searchWithPattern:nil forceReset:NO]; + + if (filesSearchDataSource.searchText.length) + { + [filesSearchDataSource searchMessages:nil force:NO]; + } + } + + [self checkAndShowBackgroundImage]; +} + +#pragma mark - UISearchBarDelegate + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText +{ + if (self.selectedViewController == recentsViewController) + { + // As the public room search is local, it can be updated on each text change + [self updateSearch]; + } + else if (self.selectedViewController == peopleSearchViewController) + { + // As the contact search is local, it can be updated on each text change + [self updateSearch]; + } + else if (!self.searchBar.text.length) + { + // Reset message search if any + [self updateSearch]; + } +} + +- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar +{ + [searchBar resignFirstResponder]; + + if (self.selectedViewController == messagesSearchViewController || self.selectedViewController == filesSearchViewController) + { + // As the messages/files search is done homeserver-side, launch it only on the "Search" button + [self updateSearch]; + } +} + +#pragma mark - ContactsTableViewControllerDelegate + +- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact +{ + // Force hiding the keyboard + [self.searchBar resignFirstResponder]; + + [[AppDelegate theDelegate].masterTabBarController selectContact:contact]; +} + +@end diff --git a/Riot/ViewController/UsersDevicesViewController.m b/Riot/ViewController/UsersDevicesViewController.m index d07017d05..17503088c 100644 --- a/Riot/ViewController/UsersDevicesViewController.m +++ b/Riot/ViewController/UsersDevicesViewController.m @@ -18,9 +18,6 @@ #import "UsersDevicesViewController.h" #import "AppDelegate.h" -#import "RageShakeManager.h" - -#import "RiotDesignValues.h" @interface UsersDevicesViewController () { diff --git a/Riot/Views/Contact/ContactTableViewCell.m b/Riot/Views/Contact/ContactTableViewCell.m index 04c01c6ec..9a3438c46 100644 --- a/Riot/Views/Contact/ContactTableViewCell.m +++ b/Riot/Views/Contact/ContactTableViewCell.m @@ -24,6 +24,8 @@ #import "AvatarGenerator.h" #import "Tools.h" +#import "NBPhoneNumberUtil.h" + @interface ContactTableViewCell() { /** @@ -34,7 +36,7 @@ @end @implementation ContactTableViewCell -@synthesize mxRoom; +@synthesize mxRoom, delegate; - (void)awakeFromNib { @@ -60,6 +62,17 @@ self.thumbnailView.clipsToBounds = YES; } +- (void)prepareForReuse +{ + [super prepareForReuse]; + + // Restore default values + self.contentView.alpha = 1; + self.userInteractionEnabled = YES; + self.accessoryType = UITableViewCellAccessoryNone; + self.accessoryView = nil; +} + - (void)setShowCustomAccessoryView:(BOOL)show { _showCustomAccessoryView = show; diff --git a/Riot/Views/Directory/DirectoryServerDetailTableViewCell.h b/Riot/Views/Directory/DirectoryServerDetailTableViewCell.h new file mode 100644 index 000000000..a240de596 --- /dev/null +++ b/Riot/Views/Directory/DirectoryServerDetailTableViewCell.h @@ -0,0 +1,26 @@ +/* + Copyright 2017 Vector Creations 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 "DirectoryServerTableViewCell.h" + +/** + The `DirectoryServerDetailTableViewCell` cell displays a homeserver . + */ +@interface DirectoryServerDetailTableViewCell : DirectoryServerTableViewCell + +@property (weak, nonatomic) IBOutlet UILabel *detailDescLabel; + +@end diff --git a/Riot/Views/Directory/DirectoryServerDetailTableViewCell.m b/Riot/Views/Directory/DirectoryServerDetailTableViewCell.m new file mode 100644 index 000000000..f785fd1d5 --- /dev/null +++ b/Riot/Views/Directory/DirectoryServerDetailTableViewCell.m @@ -0,0 +1,45 @@ +/* + Copyright 2017 Vector Creations 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 "DirectoryServerDetailTableViewCell.h" + +#import "RiotDesignValues.h" + +@implementation DirectoryServerDetailTableViewCell + +- (void)awakeFromNib +{ + [super awakeFromNib]; + + self.detailDescLabel.textColor = kRiotTextColorGray; +} + +- (void)render:(id)cellData +{ + [super render:cellData]; + + if (cellData.includeAllNetworks) + { + + self.detailDescLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"directory_server_all_rooms", @"Vector", nil), cellData.homeserver]; + } + else + { + self.detailDescLabel.text = NSLocalizedStringFromTable(@"directory_server_all_native_rooms", @"Vector", nil); + } +} + +@end diff --git a/Riot/Views/Directory/DirectoryServerDetailTableViewCell.xib b/Riot/Views/Directory/DirectoryServerDetailTableViewCell.xib new file mode 100644 index 000000000..599d80f79 --- /dev/null +++ b/Riot/Views/Directory/DirectoryServerDetailTableViewCell.xib @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Views/Directory/DirectoryServerTableViewCell.h b/Riot/Views/Directory/DirectoryServerTableViewCell.h new file mode 100644 index 000000000..a2d51e325 --- /dev/null +++ b/Riot/Views/Directory/DirectoryServerTableViewCell.h @@ -0,0 +1,43 @@ +/* + Copyright 2017 Vector Creations 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 "MXKDirectoryServerCellDataStoring.h" + +/** + The `DirectoryServerTableViewCell` cell displays a server . + */ +@interface DirectoryServerTableViewCell : MXKTableViewCell + +@property (weak, nonatomic) IBOutlet MXKImageView *iconImageView; +@property (weak, nonatomic) IBOutlet UILabel *descLabel; + +/** + Update the information displayed by the cell. + + @param cellData the data to render. + */ +- (void)render:(id)cellData; + +/** + Get the cell height. + + @return the cell height. + */ ++ (CGFloat)cellHeight; + +@end diff --git a/Riot/Views/Directory/DirectoryServerTableViewCell.m b/Riot/Views/Directory/DirectoryServerTableViewCell.m new file mode 100644 index 000000000..7f44b8a98 --- /dev/null +++ b/Riot/Views/Directory/DirectoryServerTableViewCell.m @@ -0,0 +1,67 @@ +/* + Copyright 2017 Vector Creations 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 "DirectoryServerTableViewCell.h" + +#import "AvatarGenerator.h" +#import "RiotDesignValues.h" + +@implementation DirectoryServerTableViewCell + +#pragma mark - Class methods + +- (void)awakeFromNib +{ + [super awakeFromNib]; + + self.descLabel.textColor = kRiotTextColorBlack; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + // Round image view + self.iconImageView.clipsToBounds = YES; + self.iconImageView.backgroundColor = [UIColor clearColor]; +} + +- (void)render:(id)cellData +{ + self.iconImageView.hidden = NO; + + if (cellData.icon) + { + self.iconImageView.image = cellData.icon; + } + else if (cellData.thirdPartyProtocolInstance.icon) + { + [self.iconImageView setImageURL:cellData.thirdPartyProtocolInstance.icon withType:nil andImageOrientation:UIImageOrientationUp previewImage:[UIImage imageNamed:@"placeholder"]]; + } + else + { + self.iconImageView.hidden = YES; + } + + self.descLabel.text = cellData.desc; +} + ++ (CGFloat)cellHeight +{ + return 74; +} + +@end diff --git a/Riot/Views/Directory/DirectoryServerTableViewCell.xib b/Riot/Views/Directory/DirectoryServerTableViewCell.xib new file mode 100644 index 000000000..2f5fd8909 --- /dev/null +++ b/Riot/Views/Directory/DirectoryServerTableViewCell.xib @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.h b/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.h new file mode 100644 index 000000000..292b5fec5 --- /dev/null +++ b/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.h @@ -0,0 +1,24 @@ +/* + Copyright 2017 Vector Creations 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 + +/** + `RoomEmptyBubbleCell` displays empty bubbles. + */ +@interface RoomEmptyBubbleCell : MXKRoomEmptyBubbleTableViewCell + +@end diff --git a/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.m b/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.m new file mode 100644 index 000000000..fdb0e0904 --- /dev/null +++ b/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.m @@ -0,0 +1,21 @@ +/* + Copyright 2017 Vector Creations 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 "RoomEmptyBubbleCell.h" + +@implementation RoomEmptyBubbleCell + +@end diff --git a/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.xib b/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.xib new file mode 100644 index 000000000..1b05a7e0b --- /dev/null +++ b/Riot/Views/RoomBubbleList/RoomEmptyBubbleCell.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m b/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m index eaa7fd25b..7357285ea 100644 --- a/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m +++ b/Riot/Views/RoomInputToolbar/RoomInputToolbarView.m @@ -19,6 +19,8 @@ #import "RiotDesignValues.h" +#import "GBDeviceInfo_iOS.h" + #import "UINavigationController+Riot.h" #import @@ -82,7 +84,7 @@ growingTextView.textColor = kRiotTextColorBlack; growingTextView.tintColor = kRiotColorGreen; - self.placeholder = NSLocalizedStringFromTable(@"room_message_placeholder", @"Vector", nil); + self.isEncryptionEnabled = _isEncryptionEnabled; } - (void)setSupportCallOption:(BOOL)supportCallOption @@ -106,16 +108,34 @@ - (void)setIsEncryptionEnabled:(BOOL)isEncryptionEnabled { - if (isEncryptionEnabled) + _isEncryptionEnabled = isEncryptionEnabled; + + // Consider the default placeholder + NSString *placeholder= NSLocalizedStringFromTable(@"room_message_short_placeholder", @"Vector", nil); + + if (_isEncryptionEnabled) { self.encryptedRoomIcon.image = [UIImage imageNamed:@"e2e_verified"]; + + // Check the device screen size before using large placeholder + if ([GBDeviceInfo deviceInfo].family == GBDeviceFamilyiPad || [GBDeviceInfo deviceInfo].displayInfo.display >= GBDeviceDisplay4p7Inch) + { + placeholder = NSLocalizedStringFromTable(@"encrypted_room_message_placeholder", @"Vector", nil); + } } else { self.encryptedRoomIcon.image = [UIImage imageNamed:@"e2e_unencrypted"]; + + // Check the device screen size before using large placeholder + if ([GBDeviceInfo deviceInfo].family == GBDeviceFamilyiPad || [GBDeviceInfo deviceInfo].displayInfo.display >= GBDeviceDisplay4p7Inch) + { + placeholder = NSLocalizedStringFromTable(@"room_message_placeholder", @"Vector", nil); + } } - _isEncryptionEnabled = isEncryptionEnabled; + + self.placeholder = placeholder; } - (void)setActiveCall:(BOOL)activeCall diff --git a/Riot/Views/RoomList/InviteRecentTableViewCell.xib b/Riot/Views/RoomList/InviteRecentTableViewCell.xib index 72a20e045..7b2d29c4e 100644 --- a/Riot/Views/RoomList/InviteRecentTableViewCell.xib +++ b/Riot/Views/RoomList/InviteRecentTableViewCell.xib @@ -1,11 +1,11 @@ - - + + - + @@ -16,7 +16,7 @@ - + @@ -28,6 +28,10 @@ + + - - + + + - + @@ -143,7 +141,7 @@ - + @@ -155,7 +153,6 @@ - diff --git a/Riot/Views/RoomList/PublicRoomTableViewCell.h b/Riot/Views/RoomList/PublicRoomTableViewCell.h index de23741ad..e0f145028 100644 --- a/Riot/Views/RoomList/PublicRoomTableViewCell.h +++ b/Riot/Views/RoomList/PublicRoomTableViewCell.h @@ -27,4 +27,11 @@ @property (weak, nonatomic) IBOutlet MXKImageView *roomAvatar; +/** + Get the cell height. + + @return the cell height. + */ ++ (CGFloat)cellHeight; + @end diff --git a/Riot/Views/RoomList/PublicRoomTableViewCell.m b/Riot/Views/RoomList/PublicRoomTableViewCell.m index 67e7a5711..edb9f34fc 100644 --- a/Riot/Views/RoomList/PublicRoomTableViewCell.m +++ b/Riot/Views/RoomList/PublicRoomTableViewCell.m @@ -64,4 +64,9 @@ _roomAvatar.contentMode = UIViewContentModeScaleAspectFill; } ++ (CGFloat)cellHeight +{ + return 74; +} + @end diff --git a/Riot/Views/RoomList/PublicRoomTableViewCell.xib b/Riot/Views/RoomList/PublicRoomTableViewCell.xib index c3356a6b7..4f48a2c4b 100644 --- a/Riot/Views/RoomList/PublicRoomTableViewCell.xib +++ b/Riot/Views/RoomList/PublicRoomTableViewCell.xib @@ -53,7 +53,7 @@ - + diff --git a/Riot/Views/RoomList/RecentTableViewCell.h b/Riot/Views/RoomList/RecentTableViewCell.h index 08c366e0c..7e8d85a08 100644 --- a/Riot/Views/RoomList/RecentTableViewCell.h +++ b/Riot/Views/RoomList/RecentTableViewCell.h @@ -23,7 +23,7 @@ @property (weak, nonatomic) IBOutlet UIView *missedNotifAndUnreadIndicator; @property (weak, nonatomic) IBOutlet MXKImageView *roomAvatar; -@property (weak, nonatomic) IBOutlet UIImageView *directRoomIcon; +@property (weak, nonatomic) IBOutlet UIView *directRoomBorderView; @property (weak, nonatomic) IBOutlet UIImageView *encryptedRoomIcon; @property (weak, nonatomic) IBOutlet UILabel *missedNotifAndUnreadBadgeLabel; diff --git a/Riot/Views/RoomList/RecentTableViewCell.m b/Riot/Views/RoomList/RecentTableViewCell.m index 41b2d0827..8009d7b7b 100644 --- a/Riot/Views/RoomList/RecentTableViewCell.m +++ b/Riot/Views/RoomList/RecentTableViewCell.m @@ -41,6 +41,12 @@ self.lastEventDescription.textColor = kRiotTextColorGray; self.lastEventDate.textColor = kRiotTextColorGray; self.missedNotifAndUnreadBadgeLabel.textColor = [UIColor whiteColor]; + + // Prepare direct room border + [self.directRoomBorderView.layer setCornerRadius:self.directRoomBorderView.frame.size.width / 2]; + self.directRoomBorderView.clipsToBounds = YES; + self.directRoomBorderView.layer.borderColor = CGColorCreateCopyWithAlpha(kRiotColorGreen.CGColor, 0.75); + self.directRoomBorderView.layer.borderWidth = 3; } - (void)layoutSubviews @@ -91,7 +97,7 @@ self.missedNotifAndUnreadBadgeBgView.hidden = NO; self.missedNotifAndUnreadBadgeBgView.backgroundColor = self.missedNotifAndUnreadIndicator.backgroundColor; - self.missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", roomCellData.notificationCount]; + self.missedNotifAndUnreadBadgeLabel.text = roomCellData.notificationCountStringValue; [self.missedNotifAndUnreadBadgeLabel sizeToFit]; self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = self.missedNotifAndUnreadBadgeLabel.frame.size.width + 18; @@ -127,8 +133,8 @@ } self.roomAvatar.backgroundColor = [UIColor clearColor]; - - self.directRoomIcon.hidden = !roomCellData.roomSummary.room.isDirect; + + self.directRoomBorderView.hidden = !roomCellData.roomSummary.room.isDirect; self.encryptedRoomIcon.hidden = !roomCellData.roomSummary.isEncrypted; diff --git a/Riot/Views/RoomList/RecentTableViewCell.xib b/Riot/Views/RoomList/RecentTableViewCell.xib index e9fbc3f2c..18be22a29 100644 --- a/Riot/Views/RoomList/RecentTableViewCell.xib +++ b/Riot/Views/RoomList/RecentTableViewCell.xib @@ -1,11 +1,11 @@ - - + + - + @@ -15,7 +15,7 @@ - + + - + @@ -113,20 +113,20 @@ - + - + - + @@ -140,7 +140,6 @@ - diff --git a/Riot/Views/RoomList/RoomCollectionViewCell.h b/Riot/Views/RoomList/RoomCollectionViewCell.h new file mode 100644 index 000000000..c69d1e5b3 --- /dev/null +++ b/Riot/Views/RoomList/RoomCollectionViewCell.h @@ -0,0 +1,46 @@ +/* + Copyright 2017 Vector Creations 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 + +/** + 'RoomCollectionViewCell' class is used to display a room in a collection view. + */ +@interface RoomCollectionViewCell : MXKCollectionViewCell +{ +@protected + /** + The current cell data displayed by the collection view cell + */ + id roomCellData; +} + +@property (weak, nonatomic) IBOutlet UILabel *roomTitle; + +@property (weak, nonatomic) IBOutlet UIView *directRoomBorderView; +@property (weak, nonatomic) IBOutlet MXKImageView *roomAvatar; +@property (weak, nonatomic) IBOutlet UIImageView *encryptedRoomIcon; + +@property (weak, nonatomic) IBOutlet UILabel *missedNotifAndUnreadBadgeLabel; +@property (weak, nonatomic) IBOutlet UIView *missedNotifAndUnreadBadgeBgView; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *missedNotifAndUnreadBadgeBgViewWidthConstraint; + +/** + The default collection view cell size. + */ ++ (CGSize)defaultCellSize; + +@end diff --git a/Riot/Views/RoomList/RoomCollectionViewCell.m b/Riot/Views/RoomList/RoomCollectionViewCell.m new file mode 100644 index 000000000..bdfcc1fc3 --- /dev/null +++ b/Riot/Views/RoomList/RoomCollectionViewCell.m @@ -0,0 +1,164 @@ +/* + Copyright 2017 Vector Creations 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 "RoomCollectionViewCell.h" + +#import "AvatarGenerator.h" + +#import "RiotDesignValues.h" + +#import "MXRoom+Riot.h" + +@implementation RoomCollectionViewCell + +#pragma mark - Class methods + +- (void)awakeFromNib +{ + [super awakeFromNib]; + + // Round room image view + [_roomAvatar.layer setCornerRadius:_roomAvatar.frame.size.width / 2]; + _roomAvatar.clipsToBounds = YES; + + // Initialize unread count badge + [_missedNotifAndUnreadBadgeBgView.layer setCornerRadius:10]; + _missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0; + + self.roomTitle.textColor = kRiotTextColorBlack; + self.missedNotifAndUnreadBadgeLabel.textColor = [UIColor whiteColor]; + + // Prepare direct room border + [self.directRoomBorderView.layer setCornerRadius:self.directRoomBorderView.frame.size.width / 2]; + self.directRoomBorderView.clipsToBounds = YES; + self.directRoomBorderView.layer.borderColor = CGColorCreateCopyWithAlpha(kRiotColorGreen.CGColor, 0.75); + self.directRoomBorderView.layer.borderWidth = 3; + + // Disable the user interaction on the room avatar. + self.roomAvatar.userInteractionEnabled = NO; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + +} + +- (void)render:(MXKCellData *)cellData +{ + // Hide by default missed notifications and unread widgets + self.missedNotifAndUnreadBadgeBgView.hidden = YES; + self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0; + + roomCellData = (id)cellData; + if (roomCellData) + { + // Report computed values as is + self.roomTitle.text = roomCellData.roomDisplayname; + + // Notify unreads and bing + if (roomCellData.hasUnread) + { + if (0 < roomCellData.notificationCount) + { + self.missedNotifAndUnreadBadgeBgView.hidden = NO; + self.missedNotifAndUnreadBadgeBgView.backgroundColor = roomCellData.highlightCount ? kRiotColorPinkRed : kRiotColorGreen; + + self.missedNotifAndUnreadBadgeLabel.text = roomCellData.notificationCountStringValue; + [self.missedNotifAndUnreadBadgeLabel sizeToFit]; + + self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = self.missedNotifAndUnreadBadgeLabel.frame.size.width + 18; + } + + // Use bold font for the room title + if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) + { + self.roomTitle.font = [UIFont systemFontOfSize:15 weight:UIFontWeightBold]; + } + else + { + self.roomTitle.font = [UIFont boldSystemFontOfSize:15]; + } + } + else if (roomCellData.roomSummary.room.state.membership == MXMembershipInvite) + { + self.missedNotifAndUnreadBadgeBgView.hidden = NO; + self.missedNotifAndUnreadBadgeBgView.backgroundColor = kRiotColorPinkRed; + + self.missedNotifAndUnreadBadgeLabel.text = @"!"; + [self.missedNotifAndUnreadBadgeLabel sizeToFit]; + + self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = self.missedNotifAndUnreadBadgeLabel.frame.size.width + 18; + + // Use bold font for the room title + if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) + { + self.roomTitle.font = [UIFont systemFontOfSize:15 weight:UIFontWeightBold]; + } + else + { + self.roomTitle.font = [UIFont boldSystemFontOfSize:15]; + } + } + else + { + // The room title is not bold anymore + if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) + { + self.roomTitle.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium]; + } + else + { + self.roomTitle.font = [UIFont systemFontOfSize:15]; + } + } + + self.roomAvatar.backgroundColor = [UIColor clearColor]; + + self.directRoomBorderView.hidden = !roomCellData.roomSummary.room.isDirect; + + self.encryptedRoomIcon.hidden = !roomCellData.roomSummary.isEncrypted; + + [roomCellData.roomSummary.room setRoomAvatarImageIn:self.roomAvatar]; + } +} + ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + // The height is fixed + return 100; +} + ++ (CGSize)defaultCellSize +{ + return CGSizeMake(80, 100); +} + +- (void)prepareForReuse +{ + [super prepareForReuse]; + + // Remove all gesture recognizers + while (self.gestureRecognizers.count) + { + [self removeGestureRecognizer:self.gestureRecognizers[0]]; + } + self.tag = -1; +} + +@end + diff --git a/Riot/Views/RoomList/RoomCollectionViewCell.xib b/Riot/Views/RoomList/RoomCollectionViewCell.xib new file mode 100644 index 000000000..252747abf --- /dev/null +++ b/Riot/Views/RoomList/RoomCollectionViewCell.xib @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Views/TableViewCell/TableViewCellWithCollectionView.h b/Riot/Views/TableViewCell/TableViewCellWithCollectionView.h new file mode 100644 index 000000000..d44894c2b --- /dev/null +++ b/Riot/Views/TableViewCell/TableViewCellWithCollectionView.h @@ -0,0 +1,23 @@ +/* + Copyright 2017 Vector Creations 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 TableViewCellWithCollectionView : MXKTableViewCell + +@property (strong, nonatomic) IBOutlet UICollectionView *collectionView; + +@end diff --git a/Riot/Views/TableViewCell/TableViewCellWithCollectionView.m b/Riot/Views/TableViewCell/TableViewCellWithCollectionView.m new file mode 100644 index 000000000..85cab101b --- /dev/null +++ b/Riot/Views/TableViewCell/TableViewCellWithCollectionView.m @@ -0,0 +1,31 @@ +/* + Copyright 2017 Vector Creations 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 "TableViewCellWithCollectionView.h" + +@implementation TableViewCellWithCollectionView + +- (void)prepareForReuse +{ + [super prepareForReuse]; + + self.collectionView.tag = -1; + self.collectionView.dataSource = nil; + self.collectionView.delegate = nil; +} + +@end + diff --git a/Riot/Views/TableViewCell/TableViewCellWithCollectionView.xib b/Riot/Views/TableViewCell/TableViewCellWithCollectionView.xib new file mode 100644 index 000000000..46ff0317f --- /dev/null +++ b/Riot/Views/TableViewCell/TableViewCellWithCollectionView.xib @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/third_party_licenses.html b/Riot/third_party_licenses.html index eefe4fa6d..b2c1e045e 100644 --- a/Riot/third_party_licenses.html +++ b/Riot/third_party_licenses.html @@ -84,13 +84,10 @@

  • - GHMarkdownParser (https://github.com/OliverLetterer/GHMarkdownParser) -

    GHMarkdownParser is a GitHub Flavored Markdown parser for iOS and Mac OS, based on discount. -
    Copyright (c) 2011 Oliver Letterer -

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + cmark (https://github.com/jgm/cmark) +

    CommonMark parsing and rendering library and program in C. +

    BSD2-licensed.

  • @@ -130,6 +127,22 @@ href="https://www.google.com/analytics/terms">https://www.google.com/analytics/terms

  • +
  • + GZIP (https://github.com/nicklockwood/GZIP) +

    GZIP is category on NSData that provides simple gzip compression and decompression functionality. +

    Copyright (C) 2012 Charcoal Design. +

    This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +

    Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +

    The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +

    Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +

    This notice may not be removed or altered from any source distribution. +

    +
  • WebRTC-iOS (WebRTC iOS framework)