diff --git a/CHANGES.rst b/CHANGES.rst index 3643840ba1..aefa1a39ee 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,39 @@ +Changes in Vector iOS in 0.1.4 (2016-04-26) +=============================================== + +Improvements: + * Universal link: Support universal links declared at https://vector.im. + * Room Members: Add Admin/Moderator badge on members's picture. + * Room members: Support search option #154. + * Room member details: display matrix id when user taps on display name #129. + * Expanded Header: adjust labels position when room topic is empty #134. + * Expanded Header: the height is now variable. + * Chat screen: Support room preview. + * Support room preview from email invitation. + * Chat Screen: Expand header on new created room #229. + * Chat Screen: Collapse expander header when user scrolls it down. + * Chat Screen: Keep visible the expanded header or the preview in case of screen rotation, except on iPad and iPhone 6 plus. + * Universal link: Handle universal links clicked within the app. + * Universal link: Manage email validation link as universal link + * AppDelegate: Improved popToHomeViewControllerAnimated: there is now a completion callback called when we are sure that HomeVC is the visibility VC. + * AppDelegate: Added fixURLWithSeveralHashKeys method in order to fix iOS NSURLs with several hash keys in it. + * VoIP: Show an action sheet when the user clicks on the call button. He will be able to select Voice or Video Call. + +Bug fixes: + * Store: Detect and remove corrupted room data #160. + * Cannot paginate to the origin of the room #214. + * Wrong application icon badge number #254. + * The hint text animated weirdly horizontally after i send msgs #124. + * Cancelling registration while waiting for email validation does not actually cancel it #240. + * Chat screen: lag during the history scrolling. #192. + * Chat screen: wrong attachment is opened #237. + * Add nextLink to registration link #202. + * Room members: Add a specific section INVITED #132. + * Room Members: Handle correctly the power level. + * Messages: The user should be able to shrink/expand each section (Invites, Favourites, Conversations...). + * Chat header: Room details opening is delayed #181. + * Messages: Room creation button does not respond #249. + Changes in Vector iOS in 0.1.3 (2016-04-08) =============================================== diff --git a/Podfile b/Podfile index 5a6a7b160a..7be2e0d052 100644 --- a/Podfile +++ b/Podfile @@ -8,14 +8,14 @@ target "Vector" do # Different flavours of pods to MatrixKit # The tagged version on which this version of Console has been built -#pod 'MatrixKit', '~> 0.3.5' +pod 'MatrixKit', '~> 0.3.6' # The lastest release available on the CocoaPods repository #pod 'MatrixKit' # The develop branch version -pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'develop' -pod 'MatrixKit', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'develop' +#pod 'MatrixSDK', :git => 'https://github.com/matrix-org/matrix-ios-sdk.git', :branch => 'develop' +#pod 'MatrixKit', :git => 'https://github.com/matrix-org/matrix-ios-kit.git', :branch => 'develop' # The one used for developping both MatrixSDK and MatrixKit # Note that MatrixSDK must be cloned into a folder called matrix-ios-sdk next to the MatrixKit folder diff --git a/Podfile.lock b/Podfile.lock index eaaa72a967..01f105707d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -25,33 +25,16 @@ PODS: - GBJailbreakDetection (1.3.0) - HPGrowingTextView (1.1) - libPhoneNumber-iOS (0.8.11) - - MatrixKit (0.3.5): + - MatrixKit (0.3.6): - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.8.7) - - MatrixSDK (~> 0.6.5) - - MatrixSDK (0.6.5): + - MatrixSDK (~> 0.6.6) + - MatrixSDK (0.6.6): - AFNetworking (~> 2.6.0) DEPENDENCIES: - GBDeviceInfo (~> 3.4.0) - - MatrixKit (from `https://github.com/matrix-org/matrix-ios-kit.git`, branch `develop`) - - MatrixSDK (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `develop`) - -EXTERNAL SOURCES: - MatrixKit: - :branch: develop - :git: https://github.com/matrix-org/matrix-ios-kit.git - MatrixSDK: - :branch: develop - :git: https://github.com/matrix-org/matrix-ios-sdk.git - -CHECKOUT OPTIONS: - MatrixKit: - :commit: b4cc5e3530216023fa78cadcae42624b8af1b60e - :git: https://github.com/matrix-org/matrix-ios-kit.git - MatrixSDK: - :commit: 48e2580540dbd5bf312f75d60c8922f6e2ca11d5 - :git: https://github.com/matrix-org/matrix-ios-sdk.git + - MatrixKit (~> 0.3.6) SPEC CHECKSUMS: AFNetworking: cb8d14a848e831097108418f5d49217339d4eb60 @@ -59,7 +42,7 @@ SPEC CHECKSUMS: GBJailbreakDetection: a216773574b62dddb6c876ffdb52c54ac05e27e0 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 libPhoneNumber-iOS: ded33fab2c51ee847979556aa504c9e70f32d703 - MatrixKit: defac2bf01d1397e00cf4b1a72a25ac10c2c901c - MatrixSDK: 598925faff319441724db8a6af2a9cfc21d167e5 + MatrixKit: 4f5350456d503575cd40e9a9a9e672a7bb326d85 + MatrixSDK: dcf3a2cc6478a6a1f0c8b8eaead78b74ada7c439 COCOAPODS: 0.39.0 diff --git a/Vector.xcodeproj/project.pbxproj b/Vector.xcodeproj/project.pbxproj index a863cd8962..83b1f8f029 100644 --- a/Vector.xcodeproj/project.pbxproj +++ b/Vector.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 32A887221C89B9580037DC17 /* SimpleRoomTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32A887201C89B9580037DC17 /* SimpleRoomTitleView.xib */; }; 32AAC3E61C3525DE007A3B5B /* RoomSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32AAC3E51C3525DE007A3B5B /* RoomSearchViewController.m */; }; 32AAC3E91C353CEA007A3B5B /* RoomSearchDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 32AAC3E81C353CEA007A3B5B /* RoomSearchDataSource.m */; }; + 32C52BF61CBE4B0A00863B33 /* RoomEmailInvitation.m in Sources */ = {isa = PBXBuildFile; fileRef = 32C52BF51CBE4B0A00863B33 /* RoomEmailInvitation.m */; }; + 32C52BF91CBFF50C00863B33 /* RoomPreviewData.m in Sources */ = {isa = PBXBuildFile; fileRef = 32C52BF81CBFF50C00863B33 /* RoomPreviewData.m */; }; 32D0A4BC1CA44509008F3451 /* plus_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 32D0A4B91CA44509008F3451 /* plus_icon.png */; }; 32D0A4BD1CA44509008F3451 /* plus_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 32D0A4BA1CA44509008F3451 /* plus_icon@2x.png */; }; 32D0A4BE1CA44509008F3451 /* plus_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 32D0A4BB1CA44509008F3451 /* plus_icon@3x.png */; }; @@ -133,7 +135,21 @@ F025290C1C11B6FC00E1FE1B /* voice_call_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F02528D01C11B6FC00E1FE1B /* voice_call_icon.png */; }; F025290D1C11B6FC00E1FE1B /* voice_call_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F02528D11C11B6FC00E1FE1B /* voice_call_icon@2x.png */; }; F025290E1C11B6FC00E1FE1B /* voice_call_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F02528D21C11B6FC00E1FE1B /* voice_call_icon@3x.png */; }; + F02BB04B1CBE2EE70022A025 /* PreviewRoomTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F02BB0491CBE2EE70022A025 /* PreviewRoomTitleView.m */; }; + F02BB04C1CBE2EE70022A025 /* PreviewRoomTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F02BB04A1CBE2EE70022A025 /* PreviewRoomTitleView.xib */; }; F02D87C69D1FFCD2C1531F3D /* libPods-Vector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B179239B79688A61A3F465F /* libPods-Vector.a */; }; + F03FBCC51CBBF521000A5770 /* admin_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCB31CBBF521000A5770 /* admin_icon.png */; }; + F03FBCC61CBBF521000A5770 /* admin_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCB41CBBF521000A5770 /* admin_icon@2x.png */; }; + F03FBCC71CBBF521000A5770 /* admin_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCB51CBBF521000A5770 /* admin_icon@3x.png */; }; + F03FBCCB1CBBF521000A5770 /* mod_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCB91CBBF521000A5770 /* mod_icon.png */; }; + F03FBCCC1CBBF521000A5770 /* mod_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCBA1CBBF521000A5770 /* mod_icon@2x.png */; }; + F03FBCCD1CBBF521000A5770 /* mod_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCBB1CBBF521000A5770 /* mod_icon@3x.png */; }; + F03FBCCE1CBBF521000A5770 /* shrink_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCBC1CBBF521000A5770 /* shrink_icon.png */; }; + F03FBCCF1CBBF521000A5770 /* shrink_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCBD1CBBF521000A5770 /* shrink_icon@2x.png */; }; + F03FBCD01CBBF521000A5770 /* shrink_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCBE1CBBF521000A5770 /* shrink_icon@3x.png */; }; + F03FBCD71CBC49B6000A5770 /* disclosure_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCD41CBC49B6000A5770 /* disclosure_icon.png */; }; + F03FBCD81CBC49B6000A5770 /* disclosure_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCD51CBC49B6000A5770 /* disclosure_icon@2x.png */; }; + F03FBCD91CBC49B6000A5770 /* disclosure_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F03FBCD61CBC49B6000A5770 /* disclosure_icon@3x.png */; }; F0418BE41C9067B40030356E /* edit_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0418BE31C9067B40030356E /* edit_icon@2x.png */; }; F047DBB51C576F2200952DA2 /* AuthenticationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F047DBB41C576F2200952DA2 /* AuthenticationViewController.xib */; }; F047DBB91C576F6600952DA2 /* AuthInputsView.m in Sources */ = {isa = PBXBuildFile; fileRef = F047DBB71C576F6600952DA2 /* AuthInputsView.m */; }; @@ -181,6 +197,7 @@ F09EE0081C5134BE0078712F /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F09EE0001C5134BE0078712F /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m */; }; F09EE0091C5134BE0078712F /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F09EE0011C5134BE0078712F /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib */; }; F0A1CD221B9F4BBA00F9C15C /* RoomParticipantsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0A1CD211B9F4BBA00F9C15C /* RoomParticipantsViewController.m */; }; + F0A2413A1CB7E28F00E150C3 /* RoomParticipantsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F0A241391CB7E28F00E150C3 /* RoomParticipantsViewController.xib */; }; F0BE3DF01C6CE17200AC3111 /* RoomMemberDetailsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0BE3DEF1C6CE17200AC3111 /* RoomMemberDetailsViewController.m */; }; F0BE3DF21C6CE28300AC3111 /* RoomMemberDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F0BE3DF11C6CE28300AC3111 /* RoomMemberDetailsViewController.xib */; }; F0C34B611C15C28300C36F09 /* RoomOutgoingAttachmentBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = F0C34B561C15C28300C36F09 /* RoomOutgoingAttachmentBubbleCell.m */; }; @@ -263,6 +280,10 @@ 32AAC3E51C3525DE007A3B5B /* RoomSearchViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomSearchViewController.m; sourceTree = ""; }; 32AAC3E71C353CEA007A3B5B /* RoomSearchDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomSearchDataSource.h; sourceTree = ""; }; 32AAC3E81C353CEA007A3B5B /* RoomSearchDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomSearchDataSource.m; sourceTree = ""; }; + 32C52BF41CBE4B0A00863B33 /* RoomEmailInvitation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomEmailInvitation.h; sourceTree = ""; }; + 32C52BF51CBE4B0A00863B33 /* RoomEmailInvitation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomEmailInvitation.m; sourceTree = ""; }; + 32C52BF71CBFF50C00863B33 /* RoomPreviewData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomPreviewData.h; sourceTree = ""; }; + 32C52BF81CBFF50C00863B33 /* RoomPreviewData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomPreviewData.m; sourceTree = ""; }; 32D0A4B91CA44509008F3451 /* plus_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = plus_icon.png; sourceTree = ""; }; 32D0A4BA1CA44509008F3451 /* plus_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "plus_icon@2x.png"; sourceTree = ""; }; 32D0A4BB1CA44509008F3451 /* plus_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "plus_icon@3x.png"; sourceTree = ""; }; @@ -271,6 +292,7 @@ 32D200821C15C56A00A4E396 /* search_bg@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "search_bg@3x.png"; sourceTree = ""; }; 32D200861C16C2B100A4E396 /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = ""; }; 32D200871C16C2B100A4E396 /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = ""; }; + 32F2ABFC1CB5694B00BF205F /* Vector.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Vector.entitlements; sourceTree = ""; }; 32F2E6191C230D4D003BDEA5 /* PublicRoomsDirectoryDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PublicRoomsDirectoryDataSource.h; sourceTree = ""; }; 32F2E61A1C230D4D003BDEA5 /* PublicRoomsDirectoryDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PublicRoomsDirectoryDataSource.m; sourceTree = ""; }; 435C7E1A9BC3DE28D526540F /* Pods-Vector.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Vector.release.xcconfig"; path = "Pods/Target Support Files/Pods-Vector/Pods-Vector.release.xcconfig"; sourceTree = ""; }; @@ -397,6 +419,21 @@ F02528D01C11B6FC00E1FE1B /* voice_call_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voice_call_icon.png; sourceTree = ""; }; F02528D11C11B6FC00E1FE1B /* voice_call_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "voice_call_icon@2x.png"; sourceTree = ""; }; F02528D21C11B6FC00E1FE1B /* voice_call_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "voice_call_icon@3x.png"; sourceTree = ""; }; + F02BB0481CBE2EE70022A025 /* PreviewRoomTitleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PreviewRoomTitleView.h; path = RoomTitle/PreviewRoomTitleView.h; sourceTree = ""; }; + F02BB0491CBE2EE70022A025 /* PreviewRoomTitleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PreviewRoomTitleView.m; path = RoomTitle/PreviewRoomTitleView.m; sourceTree = ""; }; + F02BB04A1CBE2EE70022A025 /* PreviewRoomTitleView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PreviewRoomTitleView.xib; path = RoomTitle/PreviewRoomTitleView.xib; sourceTree = ""; }; + F03FBCB31CBBF521000A5770 /* admin_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = admin_icon.png; sourceTree = ""; }; + F03FBCB41CBBF521000A5770 /* admin_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "admin_icon@2x.png"; sourceTree = ""; }; + F03FBCB51CBBF521000A5770 /* admin_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "admin_icon@3x.png"; sourceTree = ""; }; + F03FBCB91CBBF521000A5770 /* mod_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mod_icon.png; sourceTree = ""; }; + F03FBCBA1CBBF521000A5770 /* mod_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mod_icon@2x.png"; sourceTree = ""; }; + F03FBCBB1CBBF521000A5770 /* mod_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mod_icon@3x.png"; sourceTree = ""; }; + F03FBCBC1CBBF521000A5770 /* shrink_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = shrink_icon.png; sourceTree = ""; }; + F03FBCBD1CBBF521000A5770 /* shrink_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "shrink_icon@2x.png"; sourceTree = ""; }; + F03FBCBE1CBBF521000A5770 /* shrink_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "shrink_icon@3x.png"; sourceTree = ""; }; + F03FBCD41CBC49B6000A5770 /* disclosure_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = disclosure_icon.png; sourceTree = ""; }; + F03FBCD51CBC49B6000A5770 /* disclosure_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "disclosure_icon@2x.png"; sourceTree = ""; }; + F03FBCD61CBC49B6000A5770 /* disclosure_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "disclosure_icon@3x.png"; sourceTree = ""; }; F0418BE31C9067B40030356E /* edit_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "edit_icon@2x.png"; sourceTree = ""; }; F047DBB41C576F2200952DA2 /* AuthenticationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AuthenticationViewController.xib; sourceTree = ""; }; F047DBB61C576F6600952DA2 /* AuthInputsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthInputsView.h; sourceTree = ""; }; @@ -467,6 +504,7 @@ F09EE0011C5134BE0078712F /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib; sourceTree = ""; }; F0A1CD201B9F4BBA00F9C15C /* RoomParticipantsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomParticipantsViewController.h; sourceTree = ""; }; F0A1CD211B9F4BBA00F9C15C /* RoomParticipantsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomParticipantsViewController.m; sourceTree = ""; }; + F0A241391CB7E28F00E150C3 /* RoomParticipantsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomParticipantsViewController.xib; sourceTree = ""; }; F0BE3DEE1C6CE17200AC3111 /* RoomMemberDetailsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMemberDetailsViewController.h; sourceTree = ""; }; F0BE3DEF1C6CE17200AC3111 /* RoomMemberDetailsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMemberDetailsViewController.m; sourceTree = ""; }; F0BE3DF11C6CE28300AC3111 /* RoomMemberDetailsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomMemberDetailsViewController.xib; sourceTree = ""; }; @@ -555,6 +593,9 @@ 71046D581C0C631100DCA984 /* RoomTitle */ = { isa = PBXGroup; children = ( + F02BB0481CBE2EE70022A025 /* PreviewRoomTitleView.h */, + F02BB0491CBE2EE70022A025 /* PreviewRoomTitleView.m */, + F02BB04A1CBE2EE70022A025 /* PreviewRoomTitleView.xib */, 32A8871E1C89B9580037DC17 /* SimpleRoomTitleView.h */, 32A8871F1C89B9580037DC17 /* SimpleRoomTitleView.m */, 32A887201C89B9580037DC17 /* SimpleRoomTitleView.xib */, @@ -783,6 +824,10 @@ F05894FF1B8B7E6600B73E85 /* RoomBubbleCellData.m */, F08BE0A01B87064000C480FB /* RoomDataSource.h */, F08BE0A11B87064000C480FB /* RoomDataSource.m */, + 32C52BF41CBE4B0A00863B33 /* RoomEmailInvitation.h */, + 32C52BF51CBE4B0A00863B33 /* RoomEmailInvitation.m */, + 32C52BF71CBFF50C00863B33 /* RoomPreviewData.h */, + 32C52BF81CBFF50C00863B33 /* RoomPreviewData.m */, 32AAC3E71C353CEA007A3B5B /* RoomSearchDataSource.h */, 32AAC3E81C353CEA007A3B5B /* RoomSearchDataSource.m */, ); @@ -812,6 +857,7 @@ F094A9A41B78D8F000B1FBBF /* Vector */ = { isa = PBXGroup; children = ( + 32F2ABFC1CB5694B00BF205F /* Vector.entitlements */, F094AA071B78E42600B1FBBF /* API */, F094AA0A1B78E42600B1FBBF /* Assets */, F0C34CB51C17145F00C36F09 /* Categories */, @@ -916,6 +962,7 @@ 32D200871C16C2B100A4E396 /* HomeViewController.m */, F0A1CD201B9F4BBA00F9C15C /* RoomParticipantsViewController.h */, F0A1CD211B9F4BBA00F9C15C /* RoomParticipantsViewController.m */, + F0A241391CB7E28F00E150C3 /* RoomParticipantsViewController.xib */, F0CC4DC71C4E594C003BBE45 /* MediaAlbumContentViewController.h */, F0CC4DC81C4E594C003BBE45 /* MediaAlbumContentViewController.m */, F0CC4DC91C4E594C003BBE45 /* MediaAlbumContentViewController.xib */, @@ -976,6 +1023,18 @@ F0DD7D1B1B7AA8C900C4BE02 /* Images */ = { isa = PBXGroup; children = ( + F03FBCD41CBC49B6000A5770 /* disclosure_icon.png */, + F03FBCD51CBC49B6000A5770 /* disclosure_icon@2x.png */, + F03FBCD61CBC49B6000A5770 /* disclosure_icon@3x.png */, + F03FBCB31CBBF521000A5770 /* admin_icon.png */, + F03FBCB41CBBF521000A5770 /* admin_icon@2x.png */, + F03FBCB51CBBF521000A5770 /* admin_icon@3x.png */, + F03FBCB91CBBF521000A5770 /* mod_icon.png */, + F03FBCBA1CBBF521000A5770 /* mod_icon@2x.png */, + F03FBCBB1CBBF521000A5770 /* mod_icon@3x.png */, + F03FBCBC1CBBF521000A5770 /* shrink_icon.png */, + F03FBCBD1CBBF521000A5770 /* shrink_icon@2x.png */, + F03FBCBE1CBBF521000A5770 /* shrink_icon@3x.png */, F0F83FBE1C93089500E7D322 /* video_icon.png */, F0F83FBF1C93089500E7D322 /* video_icon@2x.png */, F0F83FC01C93089500E7D322 /* video_icon@3x.png */, @@ -1160,11 +1219,15 @@ F047DBBA1C576F6600952DA2 /* AuthInputsView.xib in Resources */, F02529001C11B6FC00E1FE1B /* settings_icon.png in Resources */, 32492DB41C293CCB00035C79 /* PublicRoomTableViewCell.xib in Resources */, + F03FBCCF1CBBF521000A5770 /* shrink_icon@2x.png in Resources */, F0F83FC21C93089500E7D322 /* video_icon@2x.png in Resources */, + F03FBCD81CBC49B6000A5770 /* disclosure_icon@2x.png in Resources */, 71B2A3BC1C2013DC00472061 /* TableViewCellWithLabelAndMXKImageView.xib in Resources */, + F03FBCD01CBBF521000A5770 /* shrink_icon@3x.png in Resources */, F02528D81C11B6FC00E1FE1B /* camera_picture.png in Resources */, F02528E01C11B6FC00E1FE1B /* create_room.png in Resources */, F02528DF1C11B6FC00E1FE1B /* camera_video.png in Resources */, + F02BB04C1CBE2EE70022A025 /* PreviewRoomTitleView.xib in Resources */, F056418C1C7CBEBD002276ED /* group.png in Resources */, F0C34B681C15C28300C36F09 /* RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, F0026B631C916E68001D2C04 /* priorityLow@3x.png in Resources */, @@ -1176,6 +1239,7 @@ F001D7631B8207C000A162C3 /* RoomInputToolbarView.xib in Resources */, F02528DE1C11B6FC00E1FE1B /* camera_switch@3x.png in Resources */, F02529091C11B6FC00E1FE1B /* upload_icon.png in Resources */, + F03FBCD91CBC49B6000A5770 /* disclosure_icon@3x.png in Resources */, F05641931C7DF9DE002276ED /* error@2x.png in Resources */, F0D2D9861C197DCB007B8C96 /* RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, F02528F21C11B6FC00E1FE1B /* remove_icon.png in Resources */, @@ -1186,6 +1250,7 @@ F02528DB1C11B6FC00E1FE1B /* camera_stop.png in Resources */, F025290E1C11B6FC00E1FE1B /* voice_call_icon@3x.png in Resources */, F02528FC1C11B6FC00E1FE1B /* selection_tick@3x.png in Resources */, + F03FBCCD1CBBF521000A5770 /* mod_icon@3x.png in Resources */, 7179284A1C03852C00407D96 /* TableViewCellWithLabelAndTextField.xib in Resources */, F025290B1C11B6FC00E1FE1B /* upload_icon@3x.png in Resources */, F02529011C11B6FC00E1FE1B /* settings_icon@2x.png in Resources */, @@ -1193,6 +1258,7 @@ F02528F91C11B6FC00E1FE1B /* search_icon@3x.png in Resources */, F0DDDBB71C5A5F55000C6C46 /* Icon-170.png in Resources */, F0DA3FCA1C4691CD0055438B /* details_icon@3x.png in Resources */, + F03FBCC61CBBF521000A5770 /* admin_icon@2x.png in Resources */, 32D200831C15C56A00A4E396 /* search_bg.png in Resources */, F02529041C11B6FC00E1FE1B /* typing@2x.png in Resources */, F0C34B641C15C28300C36F09 /* RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, @@ -1206,6 +1272,7 @@ F02528D51C11B6FC00E1FE1B /* camera_capture.png in Resources */, F05641851C7CBB58002276ED /* bubbles_bg_landscape@3x.png in Resources */, F056418E1C7CBEBD002276ED /* group@3x.png in Resources */, + F03FBCCE1CBBF521000A5770 /* shrink_icon.png in Resources */, F003AA811C690628008B430C /* RoomAvatarTitleView.xib in Resources */, F0026B551C916E68001D2C04 /* leave.png in Resources */, F0C34B701C15CA2E00C36F09 /* RoomOutgoingAttachmentWithPaginationTitleBubbleCell.xib in Resources */, @@ -1214,17 +1281,20 @@ F0BE3DF21C6CE28300AC3111 /* RoomMemberDetailsViewController.xib in Resources */, F0026B5D1C916E68001D2C04 /* notificationsOff@3x.png in Resources */, F0C34CB21C16269D00C36F09 /* RoomIncomingAttachmentWithPaginationTitleBubbleCell.xib in Resources */, + F03FBCCC1CBBF521000A5770 /* mod_icon@2x.png in Resources */, F0026B4F1C916E68001D2C04 /* favourite.png in Resources */, F02528DC1C11B6FC00E1FE1B /* camera_switch.png in Resources */, F0DA3FC91C4691CD0055438B /* details_icon@2x.png in Resources */, F09EE0051C5134BE0078712F /* RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, F09EE0031C5134BE0078712F /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, F0026B5F1C916E68001D2C04 /* priorityHigh@2x.png in Resources */, + F03FBCC51CBBF521000A5770 /* admin_icon.png in Resources */, F02528E81C11B6FC00E1FE1B /* logo@2x.png in Resources */, F0D2D98A1C197DCB007B8C96 /* RoomIncomingTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, F05641941C7DF9DE002276ED /* error@3x.png in Resources */, F02528E21C11B6FC00E1FE1B /* create_room@3x.png in Resources */, F02528E11C11B6FC00E1FE1B /* create_room@2x.png in Resources */, + F03FBCD71CBC49B6000A5770 /* disclosure_icon.png in Resources */, F094AA2E1B78E42600B1FBBF /* countryCodes.plist in Resources */, F02528DA1C11B6FC00E1FE1B /* camera_record.png in Resources */, F025290C1C11B6FC00E1FE1B /* voice_call_icon.png in Resources */, @@ -1281,6 +1351,8 @@ F0C34B721C15CA2E00C36F09 /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.xib in Resources */, F022285E1C64E356000AF23C /* RoomViewController.xib in Resources */, 32D0A4BC1CA44509008F3451 /* plus_icon.png in Resources */, + F03FBCCB1CBBF521000A5770 /* mod_icon.png in Resources */, + F03FBCC71CBBF521000A5770 /* admin_icon@3x.png in Resources */, F09EE0071C5134BE0078712F /* RoomOutgoingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, F0418BE41C9067B40030356E /* edit_icon@2x.png in Resources */, F0C34B661C15C28300C36F09 /* RoomOutgoingTextMsgBubbleCell.xib in Resources */, @@ -1289,6 +1361,7 @@ F0026B561C916E68001D2C04 /* leave@2x.png in Resources */, F094AA2C1B78E42600B1FBBF /* Vector.strings in Resources */, F0F83FC11C93089500E7D322 /* video_icon.png in Resources */, + F0A2413A1CB7E28F00E150C3 /* RoomParticipantsViewController.xib in Resources */, F0026B521C916E68001D2C04 /* favouriteOff.png in Resources */, 32A887221C89B9580037DC17 /* SimpleRoomTitleView.xib in Resources */, F02528FD1C11B6FC00E1FE1B /* selection_untick.png in Resources */, @@ -1362,6 +1435,7 @@ files = ( 3235CD881C3423070084EA40 /* HomeSearchTableViewCell.m in Sources */, F094A9AB1B78D8F000B1FBBF /* AppDelegate.m in Sources */, + 32C52BF61CBE4B0A00863B33 /* RoomEmailInvitation.m in Sources */, F047DBB91C576F6600952DA2 /* AuthInputsView.m in Sources */, 323A520B1C3183CC00010773 /* UIViewController+VectorSearch.m in Sources */, 717928491C03852C00407D96 /* TableViewCellWithLabelAndTextField.m in Sources */, @@ -1386,6 +1460,7 @@ 717928471C03852C00407D96 /* TableViewCellWithLabelAndLargeTextView.m in Sources */, F0D2D9871C197DCB007B8C96 /* RoomIncomingTextMsgBubbleCell.m in Sources */, F0BE3DF01C6CE17200AC3111 /* RoomMemberDetailsViewController.m in Sources */, + 32C52BF91CBFF50C00863B33 /* RoomPreviewData.m in Sources */, F0C34B6F1C15CA2E00C36F09 /* RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m in Sources */, 7165A25B1C05CD42003635D7 /* SegmentedViewController.m in Sources */, F09E24ED1C6DE24900D39503 /* RoomMemberTitleView.m in Sources */, @@ -1421,6 +1496,7 @@ 71B2A3BB1C2013DC00472061 /* TableViewCellWithLabelAndMXKImageView.m in Sources */, F0D2D9851C197DCB007B8C96 /* RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m in Sources */, 71046D5E1C0C639300DCA984 /* RoomTitleView.m in Sources */, + F02BB04B1CBE2EE70022A025 /* PreviewRoomTitleView.m in Sources */, F0C34B631C15C28300C36F09 /* RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m in Sources */, F09EE0061C5134BE0078712F /* RoomOutgoingTextMsgWithoutSenderNameBubbleCell.m in Sources */, F09EE0081C5134BE0078712F /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, @@ -1571,6 +1647,7 @@ baseConfigurationReference = 11865E69C29698A4179E1F3F /* Pods-Vector.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Vector/Vector.entitlements; ENABLE_BITCODE = NO; INFOPLIST_FILE = Vector/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1585,6 +1662,7 @@ baseConfigurationReference = 435C7E1A9BC3DE28D526540F /* Pods-Vector.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Vector/Vector.entitlements; ENABLE_BITCODE = NO; INFOPLIST_FILE = Vector/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; diff --git a/Vector/AppDelegate.h b/Vector/AppDelegate.h index 006d2edb09..0c66bd25a8 100644 --- a/Vector/AppDelegate.h +++ b/Vector/AppDelegate.h @@ -19,7 +19,7 @@ #import "HomeViewController.h" -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder { BOOL isAPNSRegistered; @@ -81,10 +81,39 @@ #pragma mark - Matrix Room handling -- (void)showRoom:(NSString*)roomId withMatrixSession:(MXSession*)mxSession; +- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession; // Reopen an existing private OneToOne room with this userId or creates a new one (if it doesn't exist) - (void)startPrivateOneToOneRoomWithUserId:(NSString*)userId completion:(void (^)(void))completion; +#pragma mark - Universal link + +/** + Detect if a URL is a universal link for the application. + + @return YES if the URL can be handled by the app. + */ +- (BOOL)isUniversalLink:(NSURL*)url; + +/** + Process the fragment part of a vector.im link. + + @param fragment the fragment part of the universal link. + @return YES in case of processing success. + */ +- (BOOL)handleUniversalLinkFragment:(NSString*)fragment; + +/** + Fix a http://vector.im path url. + + This method fixes the issue with iOS which handles URL badly when there are several hash + keys ('%23') in the link. + Vector.im links have often several hash keys... + + @param url a NSURL with possibly several hash keys and thus badly parsed. + @return a NSURL correctly parsed. + */ ++ (NSURL*)fixURLWithSeveralHashKeys:(NSURL*)url; + @end diff --git a/Vector/AppDelegate.m b/Vector/AppDelegate.m index abc2fa3a20..041204b7a8 100644 --- a/Vector/AppDelegate.m +++ b/Vector/AppDelegate.m @@ -28,6 +28,7 @@ #import "RageShakeManager.h" #import "NSBundle+MatrixKit.h" +#import "MatrixSDK/MatrixSDK.h" #import "AFNetworkReachabilityManager.h" @@ -100,6 +101,24 @@ The current call view controller (if any). The room id of the current handled remote notification (if any) */ NSString *remoteNotificationRoomId; + + /** + The fragment of the universal link being processing. + Only one fragment is handled at a time. + */ + NSString *universalLinkFragmentPending; + + /** + An universal link may need to wait for an account to be logged in or for a + session to be running. Hence, this observer. + */ + id universalLinkWaitingObserver; + + /** + Completion block called when [self popToHomeViewControllerAnimated:] has been + completed. + */ + void (^popToHomeViewControllerCompletion)(); } @property (strong, nonatomic) MXKAlert *mxInAppNotification; @@ -322,6 +341,9 @@ - (void)applicationDidEnterBackground:(UIApplication *)application [self.mxInAppNotification dismiss:NO]; self.mxInAppNotification = nil; } + + // Discard any process on pending universal link + [self resetPendingUniversalLink]; // Suspend all running matrix sessions NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; @@ -423,6 +445,18 @@ - (void)applicationWillTerminate:(UIApplication *)application // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler +{ + BOOL continueUserActivity; + + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) + { + continueUserActivity = [self handleUniversalLink:userActivity]; + } + + return continueUserActivity; +} + #pragma mark - Application layout handling - (void)restoreInitialDisplay:(void (^)())completion @@ -433,23 +467,12 @@ - (void)restoreInitialDisplay:(void (^)())completion // Do it asynchronously to avoid hasardous dispatch_async after calling restoreInitialDisplay [self.window.rootViewController dismissViewControllerAnimated:NO completion:^{ - [self popToHomeViewControllerAnimated:NO]; - - // Dispatch the completion in order to let navigation stack refresh itself. - dispatch_async(dispatch_get_main_queue(), ^{ - completion(); - }); - + [self popToHomeViewControllerAnimated:NO completion:completion]; }]; } else { - [self popToHomeViewControllerAnimated:NO]; - - // Dispatch the completion in order to let navigation stack refresh itself. - dispatch_async(dispatch_get_main_queue(), ^{ - completion(); - }); + [self popToHomeViewControllerAnimated:NO completion:completion]; } } @@ -505,11 +528,16 @@ - (MXKAlert*)showErrorAsAlert:(NSError*)error #pragma mark -- (void)popToHomeViewControllerAnimated:(BOOL)animated +- (void)popToHomeViewControllerAnimated:(BOOL)animated completion:(void (^)())completion { - // Force back to the main screen - if (_homeViewController) + // Force back to the main screen if this is the not the one that is displayed + if (_homeViewController && _homeViewController != _homeNavigationController.visibleViewController) { + // Listen to the homeNavigationController changes + // We need to be sure that homeViewController is back to the screen + popToHomeViewControllerCompletion = completion; + _homeNavigationController.delegate = self; + [_homeNavigationController popToViewController:_homeViewController animated:animated]; // For unknown reason, the navigation bar is not restored correctly by [popToViewController:animated:] @@ -526,6 +554,34 @@ - (void)popToHomeViewControllerAnimated:(BOOL)animated // Release the current selected room [_homeViewController closeSelectedRoom]; } + else + { + // Dispatch the completion in order to let navigation stack refresh itself + // It is required to display the auth VC at startup + dispatch_async(dispatch_get_main_queue(), ^{ + completion(); + }); + } +} + +#pragma mark - UINavigationController delegate + +- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated +{ + if (viewController == _homeViewController) + { + _homeNavigationController.delegate = nil; + if (popToHomeViewControllerCompletion) + { + void (^popToHomeViewControllerCompletion2)() = popToHomeViewControllerCompletion; + popToHomeViewControllerCompletion = nil; + + // Dispatch the completion in order to let navigation stack refresh itself. + dispatch_async(dispatch_get_main_queue(), ^{ + popToHomeViewControllerCompletion2(); + }); + } + } } #pragma mark - APNS methods @@ -622,7 +678,7 @@ - (void)application:(UIApplication*)application didReceiveRemoteNotification:(NS NSLog(@"[AppDelegate] didReceiveRemoteNotification: open the roomViewController %@", roomId); - [self showRoom:roomId withMatrixSession:dedicatedAccount.mxSession]; + [self showRoom:roomId andEventId:nil withMatrixSession:dedicatedAccount.mxSession]; } else { @@ -667,9 +723,348 @@ - (void)application:(UIApplication*)application didReceiveRemoteNotification:(NS - (void)refreshApplicationIconBadgeNumber { - NSLog(@"[AppDelegate] refreshApplicationIconBadgeNumber"); + NSUInteger count = [MXKRoomDataSourceManager notificationCount]; + NSLog(@"[AppDelegate] refreshApplicationIconBadgeNumber: %tu", count); - [UIApplication sharedApplication].applicationIconBadgeNumber = [MXKRoomDataSourceManager notificationCount]; + [UIApplication sharedApplication].applicationIconBadgeNumber = count; +} + +#pragma mark - Universal link + +- (BOOL)isUniversalLink:(NSURL*)url +{ + BOOL isUniversalLink; + + if ([url.host isEqualToString:@"vector.im"] + && NSNotFound != [@[@"/app", @"/staging", @"/beta", @"/develop"] indexOfObject:url.path]) + { + isUniversalLink = YES; + } + + return isUniversalLink; +} + +- (BOOL)handleUniversalLink:(NSUserActivity*)userActivity +{ + NSURL *webURL = userActivity.webpageURL; + NSLog(@"[AppDelegate] handleUniversalLink: %@", webURL.absoluteString); + + // iOS Patch: fix vector.im urls before using it + webURL = [AppDelegate fixURLWithSeveralHashKeys:webURL]; + + // Manage email validation link + if ([webURL.path isEqualToString:@"/_matrix/identity/api/v1/validate/email/submitToken"]) + { + // Validate the email on the passed identity server + NSString *identityServer = [NSString stringWithFormat:@"%@://%@", webURL.scheme, webURL.host]; + MXRestClient *identityRestClient = [[MXRestClient alloc] initWithHomeServer:identityServer andOnUnrecognizedCertificateBlock:nil]; + + // Extract required parameters from the link + NSArray *pathParams; + NSMutableDictionary *queryParams; + [self parseUniversalLinkFragment:webURL.absoluteString outPathParams:&pathParams outQueryParams:&queryParams]; + + [identityRestClient submitEmailValidationToken:queryParams[@"token"] clientSecret:queryParams[@"client_secret"] sid:queryParams[@"sid"] success:^{ + + NSLog(@"[AppDelegate] handleUniversalLink. Email successfully validated."); + + if (queryParams[@"nextLink"]) + { + // Continue the registration with the passed nextLink + NSLog(@"[AppDelegate] handleUniversalLink. Complete registration with nextLink"); + NSURL *nextLink = [NSURL URLWithString:queryParams[@"nextLink"]]; + [self handleUniversalLinkFragment:nextLink.fragment]; + } + else + { + // No nextLink in Vector world means validation for binding a new email + NSLog(@"[AppDelegate] handleUniversalLink. TODO: Complete email binding"); + } + + } failure:^(NSError *error) { + + NSLog(@"[AppDelegate] handleUniversalLink. Error: submitToken failed: %@", error); + [self showErrorAsAlert:error]; + + }]; + + return YES; + } + + return [self handleUniversalLinkFragment:webURL.fragment]; +} + +- (BOOL)handleUniversalLinkFragment:(NSString*)fragment +{ + BOOL continueUserActivity = NO; + MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; + + NSLog(@"[AppDelegate] Universal link: handleUniversalLinkFragment: %@", fragment); + + // The app manages only one universal link at a time + // Discard any pending one + [self resetPendingUniversalLink]; + + // Extract params + NSArray *pathParams; + NSMutableDictionary *queryParams; + [self parseUniversalLinkFragment:fragment outPathParams:&pathParams outQueryParams:&queryParams]; + + // Sanity check + if (!pathParams.count) + { + NSLog(@"[AppDelegate] Universal link: Error: No path parameters"); + return NO; + } + + // Check the action to do + if ([pathParams[0] isEqualToString:@"room"] && pathParams.count >= 2) + { + if (accountManager.activeAccounts.count) + { + // The link is the form of "/room/[roomIdOrAlias]" or "/room/[roomIdOrAlias]/[eventId]" + NSString *roomIdOrAlias = pathParams[1]; + + // Is it a link to an event of a room? + NSString *eventId = (pathParams.count >= 3) ? pathParams[2] : nil; + + // Check there is an account that knows this room + MXKAccount *account = [accountManager accountKnowingRoomWithRoomIdOrAlias:roomIdOrAlias]; + if (account) + { + NSString *roomId = roomIdOrAlias; + + // Translate the alias into the room id + if ([roomIdOrAlias hasPrefix:@"#"]) + { + MXRoom *room = [account.mxSession roomWithAlias:roomIdOrAlias]; + if (room) + { + roomId = room.roomId; + } + } + + // Open the room page + [self showRoom:roomId andEventId:eventId withMatrixSession:account.mxSession]; + + continueUserActivity = YES; + } + else + { + // 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:@"#"]) + { + // 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]]; + [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) + { + // 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]; + } + else + { + roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias andSession:account.mxSession]; + + // Try to get more information about the room before opening its preview + [roomPreviewData fetchPreviewData:^(BOOL successed) { + + // Note: the activity indicator will not disappear if the session is not ready + [_homeViewController stopActivityIndicator]; + + [self showRoomPreview:roomPreviewData]; + }]; + } + } + }]; + + // Let's say we are handling the case + continueUserActivity = YES; + } + } + else + { + // There is no account. The app will display the AuthenticationVC. + // Wait for a successful login + NSLog(@"[AppDelegate] Universal link: The user is not logged in. Wait for a successful login"); + universalLinkFragmentPending = fragment; + + // Register an observer in order to handle new account + universalLinkWaitingObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountManagerDidAddAccountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + + // Check that 'fragment' has not been cancelled + if ([universalLinkFragmentPending isEqualToString:fragment]) + { + NSLog(@"[AppDelegate] Universal link: The user is now logged in. Retry the link"); + [self handleUniversalLinkFragment:fragment]; + } + }]; + } + } + else if ([pathParams[0] isEqualToString:@"register"]) + { + NSLog(@"[AppDelegate] Universal link with registration parameters"); + continueUserActivity = YES; + + [_homeViewController showAuthenticationScreenWithRegistrationParameters:queryParams]; + } + else + { + // Unknown command: Do nothing except coming back to the main screen + NSLog(@"[AppDelegate] Universal link: TODO: Do not know what to do with the link arguments: %@", pathParams); + + [self popToHomeViewControllerAnimated:NO completion:nil]; + } + + return continueUserActivity; +} + +- (void)resetPendingUniversalLink +{ + universalLinkFragmentPending = nil; + if (universalLinkWaitingObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:universalLinkWaitingObserver]; + universalLinkWaitingObserver = nil; + } +} + +/** + Extract params from the URL fragment part (after '#') of a vector.im Universal link: + + The fragment can contain a '?'. So there are two kinds of parameters: path params and query params. + It is in the form of /[pathParam1]/[pathParam2]?[queryParam1Key]=[queryParam1Value]&[queryParam2Key]=[queryParam2Value] + + @param fragment the fragment to parse. + @param outPathParams the decoded path params. + @param outQueryParams the decoded query params. If there is no query params, it will be nil. + */ +- (void)parseUniversalLinkFragment:(NSString*)fragment outPathParams:(NSArray **)outPathParams outQueryParams:(NSMutableDictionary **)outQueryParams +{ + NSParameterAssert(outPathParams && outQueryParams); + + NSArray *pathParams; + NSMutableDictionary *queryParams; + + NSArray *fragments = [fragment componentsSeparatedByString:@"?"]; + + // Extract path params + pathParams = [fragments[0] componentsSeparatedByString:@"/"]; + + // Remove the first empty path param string + pathParams = [pathParams filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"length > 0"]]; + + // URL decode each path param + NSMutableArray *pathParams2 = [NSMutableArray arrayWithArray:pathParams]; + for (NSInteger i = 0; i < pathParams.count; i++) + { + pathParams2[i] = [pathParams2[i] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + } + pathParams = pathParams2; + + // Extract query params if any + if (fragments.count == 2) + { + queryParams = [[NSMutableDictionary alloc] init]; + for (NSString *keyValue in [fragments[1] componentsSeparatedByString:@"&"]) + { + // Get the parameter name + NSString *key = [[keyValue componentsSeparatedByString:@"="] objectAtIndex:0]; + + // Get the parameter value + NSString *value = [[keyValue componentsSeparatedByString:@"="] objectAtIndex:1]; + if (value.length) + { + value = [value stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + value = [value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + queryParams[key] = value; + } + } + } + + *outPathParams = pathParams; + *outQueryParams = queryParams; +} + ++ (NSURL *)fixURLWithSeveralHashKeys:(NSURL *)url +{ + NSURL *fixedURL = url; + + // The NSURL may have no fragment because it contains more that '%23' occurence + if (!url.fragment) + { + // Replacing the first '%23' occurence into a '#' makes NSURL works correctly + NSString *urlString = url.absoluteString; + NSRange range = [urlString rangeOfString:@"%23"]; + if (NSNotFound != range.location) + { + urlString = [urlString stringByReplacingCharactersInRange:range withString:@"#"]; + fixedURL = [NSURL URLWithString:urlString]; + } + } + + return fixedURL; } #pragma mark - Matrix sessions handling @@ -893,7 +1288,7 @@ - (void)reloadMatrixSessions:(BOOL)clearCache } // Force back to Recents list if room details is displayed (Room details are not available until the end of initial sync) - [self popToHomeViewControllerAnimated:NO]; + [self popToHomeViewControllerAnimated:NO completion:nil]; if (clearCache) { @@ -1036,7 +1431,7 @@ - (void)enableInAppNotificationsForAccount:(MXKAccount*)account { weakSelf.mxInAppNotification = nil; // Show the room - [weakSelf showRoom:event.roomId withMatrixSession:account.mxSession]; + [weakSelf showRoom:event.roomId andEventId:nil withMatrixSession:account.mxSession]; }]; [self.mxInAppNotification showInViewController:self.window.rootViewController]; @@ -1110,16 +1505,23 @@ - (void)selectMatrixAccount:(void (^)(MXKAccount *selectedAccount))onSelection #pragma mark - Matrix Rooms handling -- (void)showRoom:(NSString*)roomId withMatrixSession:(MXSession*)mxSession +- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession { [self restoreInitialDisplay:^{ // Select room to display its details (dispatch this action in order to let TabBarController end its refresh) - [_homeViewController selectRoomWithId:roomId inMatrixSession:mxSession]; + [_homeViewController selectRoomWithId:roomId andEventId:eventId inMatrixSession:mxSession]; }]; } +- (void)showRoomPreview:(RoomPreviewData*)roomPreviewData +{ + [self restoreInitialDisplay:^{ + [_homeViewController showRoomPreview:roomPreviewData]; + }]; +} + - (void)setVisibleRoomId:(NSString *)roomId { if (roomId) @@ -1150,7 +1552,7 @@ - (void)startPrivateOneToOneRoomWithUserId:(NSString*)userId completion:(void (^ if (mxRoom) { // open it - [self showRoom:mxRoom.state.roomId withMatrixSession:mxSession]; + [self showRoom:mxRoom.state.roomId andEventId:nil withMatrixSession:mxSession]; if (completion) { @@ -1183,7 +1585,7 @@ - (void)startPrivateOneToOneRoomWithUserId:(NSString*)userId completion:(void (^ } // Open created room - [self showRoom:room.state.roomId withMatrixSession:mxSession]; + [self showRoom:room.state.roomId andEventId:nil withMatrixSession:mxSession]; if (completion) { diff --git a/Vector/Assets/Images/admin_icon.png b/Vector/Assets/Images/admin_icon.png new file mode 100644 index 0000000000..b30fcdea6a Binary files /dev/null and b/Vector/Assets/Images/admin_icon.png differ diff --git a/Vector/Assets/Images/admin_icon@2x.png b/Vector/Assets/Images/admin_icon@2x.png new file mode 100644 index 0000000000..2ace230e96 Binary files /dev/null and b/Vector/Assets/Images/admin_icon@2x.png differ diff --git a/Vector/Assets/Images/admin_icon@3x.png b/Vector/Assets/Images/admin_icon@3x.png new file mode 100644 index 0000000000..ce9e1f09d7 Binary files /dev/null and b/Vector/Assets/Images/admin_icon@3x.png differ diff --git a/Vector/Assets/Images/disclosure_icon.png b/Vector/Assets/Images/disclosure_icon.png new file mode 100644 index 0000000000..2981d794de Binary files /dev/null and b/Vector/Assets/Images/disclosure_icon.png differ diff --git a/Vector/Assets/Images/disclosure_icon@2x.png b/Vector/Assets/Images/disclosure_icon@2x.png new file mode 100644 index 0000000000..7ed1663c67 Binary files /dev/null and b/Vector/Assets/Images/disclosure_icon@2x.png differ diff --git a/Vector/Assets/Images/disclosure_icon@3x.png b/Vector/Assets/Images/disclosure_icon@3x.png new file mode 100644 index 0000000000..07fdc39610 Binary files /dev/null and b/Vector/Assets/Images/disclosure_icon@3x.png differ diff --git a/Vector/Assets/Images/mod_icon.png b/Vector/Assets/Images/mod_icon.png new file mode 100644 index 0000000000..687469595c Binary files /dev/null and b/Vector/Assets/Images/mod_icon.png differ diff --git a/Vector/Assets/Images/mod_icon@2x.png b/Vector/Assets/Images/mod_icon@2x.png new file mode 100644 index 0000000000..a08547ea05 Binary files /dev/null and b/Vector/Assets/Images/mod_icon@2x.png differ diff --git a/Vector/Assets/Images/mod_icon@3x.png b/Vector/Assets/Images/mod_icon@3x.png new file mode 100644 index 0000000000..a1a3b3d2b7 Binary files /dev/null and b/Vector/Assets/Images/mod_icon@3x.png differ diff --git a/Vector/Assets/Images/shrink_icon.png b/Vector/Assets/Images/shrink_icon.png new file mode 100644 index 0000000000..ab762f0d45 Binary files /dev/null and b/Vector/Assets/Images/shrink_icon.png differ diff --git a/Vector/Assets/Images/shrink_icon@2x.png b/Vector/Assets/Images/shrink_icon@2x.png new file mode 100644 index 0000000000..b263735068 Binary files /dev/null and b/Vector/Assets/Images/shrink_icon@2x.png differ diff --git a/Vector/Assets/Images/shrink_icon@3x.png b/Vector/Assets/Images/shrink_icon@3x.png new file mode 100644 index 0000000000..044ae11764 Binary files /dev/null and b/Vector/Assets/Images/shrink_icon@3x.png differ diff --git a/Vector/Assets/en.lproj/Vector.strings b/Vector/Assets/en.lproj/Vector.strings index 93e93f50ae..71b93f9bf8 100644 --- a/Vector/Assets/en.lproj/Vector.strings +++ b/Vector/Assets/en.lproj/Vector.strings @@ -33,8 +33,11 @@ "cancel" = "Cancel"; "save" = "Save"; "join" = "Join"; -"reject" = "Reject"; +"decline" = "Decline"; +"preview" = "Preview"; "camera" = "Camera"; +"voice" = "Voice"; +"video" = "Video"; // Authentication "auth_login" = "Log in"; @@ -73,6 +76,7 @@ "room_creation_make_public_prompt_msg" = "Are you sure you want to make this chat public? Anyone can read your messages and join the chat."; "room_creation_keep_private" = "Keep private"; "room_creation_make_private" = "Make private"; +"room_creation_wait_for_creation" = "A room is already being created. Please wait."; // Room recents "room_recents_directory" = "DIRECTORY"; @@ -102,17 +106,17 @@ "room_participants_remove_prompt_title" = "Remove?"; "room_participants_remove_prompt_msg" = "Are you sure you want to remove %@ from this chat?"; "room_participants_admin_name" = "%@ (admin)"; -"room_participants_invite_another_user" = "Invite by name, email, id"; +"room_participants_invite_another_user" = "Invite/search by name, email, id"; "room_participants_invite_malformed_id_title" = "Invite Error"; "room_participants_invite_malformed_id" = "Malformed ID. Should be an email address or a Matrix ID like '@localpart:domain'"; +"room_participants_invited_section" = "INVITED"; + "room_participants_active" = "Online"; -"room_participants_invite" = "Invite"; -"room_participants_leave" = "Left"; -"room_participants_ban" = "Banned"; "room_participants_active_less_1_hour" = "Offline"; "room_participants_active_less_x_hours" = "Offline %luh ago"; "room_participants_active_less_x_days" = "Offline %lu days ago"; +"room_participants_offline" = "Offline"; "room_participants_action_invite" = "Invite"; "room_participants_action_leave" = "Leave this room"; @@ -149,6 +153,15 @@ "room_title_new_room" = "New room"; "room_title_multiple_active_members" = "%d/%d active members"; "room_title_one_active_member" = "%d/%d active member"; +"room_title_members" = "%d members"; +"room_title_one_member" = "1 member"; + +// Room Preview +"room_preview_invitation_format" = "You have been invited to join this room by %@"; +"room_preview_subtitle" = "This is a preview of this room. Files and other attachments have been disabled."; +"room_preview_unlinked_email_warning" = "This invitation was sent to %@, which is not associated with this account.\nYou may wish to login with a different account, or add this email to your this account."; +"room_preview_try_join_an_unknown_room" = "You are trying to access %@. Would you like to join in order to participate in the discussion?"; +"room_preview_try_join_an_unknown_room_default" = "a room"; // Settings "account_logout_all" = "Logout all accounts"; diff --git a/Vector/Info.plist b/Vector/Info.plist index 793f76fac4..00ad0ca096 100644 --- a/Vector/Info.plist +++ b/Vector/Info.plist @@ -34,7 +34,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.3 + 0.1.4 CFBundleSignature ???? CFBundleVersion diff --git a/Vector/Model/Contact/Contact.h b/Vector/Model/Contact/Contact.h index 22ff9a28df..6190d44079 100644 --- a/Vector/Model/Contact/Contact.h +++ b/Vector/Model/Contact/Contact.h @@ -20,4 +20,8 @@ @property (nonatomic) MXRoomMember* mxMember; +@property (nonatomic) MXRoomThirdPartyInvite* mxThirdPartyInvite; + +@property (nonatomic) NSString* sortingDisplayName; + @end \ No newline at end of file diff --git a/Vector/Model/Contact/Contact.m b/Vector/Model/Contact/Contact.m index 5a7d956955..9e9b42cd33 100644 --- a/Vector/Model/Contact/Contact.m +++ b/Vector/Model/Contact/Contact.m @@ -43,4 +43,24 @@ - (UIImage*)thumbnailWithPreferedSize:(CGSize)size return thumbnail; } +- (NSString*)sortingDisplayName +{ + if (!_sortingDisplayName) + { + // Sanity check - display name should not be nil here + if (self.displayName) + { + NSCharacterSet *specialCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"_!~`@#$%^&*-+();:={}[],.<>?\\/\"\'"]; + + _sortingDisplayName = [self.displayName stringByTrimmingCharactersInSet:specialCharacterSet]; + } + else + { + return @""; + } + } + + return _sortingDisplayName; +} + @end diff --git a/Vector/Model/Room/RoomEmailInvitation.h b/Vector/Model/Room/RoomEmailInvitation.h new file mode 100644 index 0000000000..813fc29ec5 --- /dev/null +++ b/Vector/Model/Room/RoomEmailInvitation.h @@ -0,0 +1,45 @@ +/* + Copyright 2016 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +/** + The `RoomEmailInvitation` represents the information extracted from the link in an + invitation email. + */ +@interface RoomEmailInvitation : NSObject + +/** + The invitation parameters. + Can be nil. + */ +@property (nonatomic, readonly) NSString *email; +@property (nonatomic, readonly) NSString *signUrl; +@property (nonatomic, readonly) NSString *roomName; +@property (nonatomic, readonly) NSString *roomAvatarUrl; +@property (nonatomic, readonly) NSString *inviterName; +@property (nonatomic, readonly) NSString *guestAccessToken; +@property (nonatomic, readonly) NSString *guestUserId; + + +/** + Contructor and parser of the query params of the email link. + + @param params the query parameters extracted from the link. + */ +- (instancetype)initWithParams:(NSDictionary*)params; + +@end diff --git a/Vector/Model/Room/RoomEmailInvitation.m b/Vector/Model/Room/RoomEmailInvitation.m new file mode 100644 index 0000000000..3ba66b0d29 --- /dev/null +++ b/Vector/Model/Room/RoomEmailInvitation.m @@ -0,0 +1,39 @@ +/* + Copyright 2016 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "RoomEmailInvitation.h" + +@implementation RoomEmailInvitation + +- (instancetype)initWithParams:(NSDictionary *)params +{ + self = [super init]; + if (self) + { + if (params) + { + _email = params[@"email"]; + _signUrl = params[@"signurl"]; + _roomName = params[@"room_name"]; + _roomAvatarUrl = params[@"room_avatar_url"]; + _inviterName = params[@"inviter_name"]; + _guestAccessToken = params[@"guest_access_token"]; + _guestUserId = params[@"guest_user_id"]; + } + } + return self; +} +@end diff --git a/Vector/Model/Room/RoomPreviewData.h b/Vector/Model/Room/RoomPreviewData.h new file mode 100644 index 0000000000..11a0c0dad7 --- /dev/null +++ b/Vector/Model/Room/RoomPreviewData.h @@ -0,0 +1,81 @@ +/* + Copyright 2016 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import "RoomEmailInvitation.h" +#import "MXSession.h" + +/** + The `RoomEmailInvitation` gathers information for displaying the preview of a + room that is unknown for the user. + + Such room can come from an email invitation link or a link to a room. + */ + +@interface RoomPreviewData : NSObject + +/** + The id of the room to preview. + */ +@property (nonatomic, readonly) NSString *roomId; + +/** + In case of email invitation, the information extracted from the email invitation link. + */ +@property (nonatomic, readonly) RoomEmailInvitation *emailInvitation; + +/** + The matrix session to show the data. + */ +@property (nonatomic) MXSession *mxSession; + +/** + Preview information. + They come from the `emailInvitationParams` or [self fetchPreviewData]. + */ +@property (nonatomic, readonly) NSString *roomName; +@property (nonatomic, readonly) NSString *roomAvatarUrl; + +/** + A snapshot of the room state. + Note: This ivar may be replaced by a RoomDataSource ivar when the room preview will be + fully implemented. + */ +@property (nonatomic, readonly) MXRoomState *roomState; + +/** + Contructors. + + @param roomId the id of the room. + @param emailInvitationParams, in case of an email invitation link, the query parameters extracted from the link. + @param mxSession the session to open the room preview with. + */ +- (instancetype)initWithRoomId:(NSString*)roomId andSession:(MXSession*)mxSession; +- (instancetype)initWithRoomId:(NSString*)roomId emailInvitationParams:(NSDictionary*)emailInvitationParams andSession:(MXSession*)mxSession; + +/** + Attempt to get more information from the homeserver about the room. + + NOTE: This method is temporary while we do not support the full room preview + with preview of messages. + + @param completion the block called when the request is complete. `successed` means + the homeserver provided some information. + */ +- (void)fetchPreviewData:(void (^)(BOOL successed))completion; + +@end diff --git a/Vector/Model/Room/RoomPreviewData.m b/Vector/Model/Room/RoomPreviewData.m new file mode 100644 index 0000000000..70f5e669b6 --- /dev/null +++ b/Vector/Model/Room/RoomPreviewData.m @@ -0,0 +1,70 @@ +/* + Copyright 2016 OpenMarket Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "RoomPreviewData.h" + +@implementation RoomPreviewData + +- (instancetype)initWithRoomId:(NSString *)roomId andSession:(MXSession *)mxSession +{ + self = [super init]; + if (self) + { + _roomId = roomId; + _mxSession = mxSession; + } + return self; +} + +- (instancetype)initWithRoomId:(NSString *)roomId emailInvitationParams:(NSDictionary *)emailInvitationParams andSession:(MXSession *)mxSession +{ + self = [self initWithRoomId:roomId andSession:mxSession]; + if (self) + { + _emailInvitation = [[RoomEmailInvitation alloc] initWithParams:emailInvitationParams]; + + // Report decoded data + _roomName = _emailInvitation.roomName; + _roomAvatarUrl = _emailInvitation.roomAvatarUrl; + } + return self; +} + +- (void)fetchPreviewData:(void (^)(BOOL))completion +{ + // Make an /initialSync request to get preview data + [_mxSession.matrixRestClient initialSyncOfRoom:_roomId withLimit:0 success:^(MXRoomInitialSync *roomInitialSync) { + + _roomState = [[MXRoomState alloc] initWithRoomId:_roomId andMatrixSession:_mxSession andDirection:YES]; + + // Make roomState digest state events of the room + for (MXEvent *stateEvent in roomInitialSync.state) + { + [_roomState handleStateEvent:stateEvent]; + } + + // Report retrieved data + _roomName = _roomState.displayname; + _roomAvatarUrl = _roomState.avatar; + + completion(YES); + + } failure:^(NSError *error) { + completion(NO); + }]; +} + +@end diff --git a/Vector/Model/RoomList/RecentsDataSource.m b/Vector/Model/RoomList/RecentsDataSource.m index b00a2b07ce..13009bf5e4 100644 --- a/Vector/Model/RoomList/RecentsDataSource.m +++ b/Vector/Model/RoomList/RecentsDataSource.m @@ -29,6 +29,12 @@ #import "RecentCellData.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 + @interface RecentsDataSource() { NSMutableArray* invitesCellDataArray; @@ -43,6 +49,8 @@ @interface RecentsDataSource() NSInteger lowPrioritySection; NSInteger sectionsCount; + NSInteger shrinkedSectionsBitMask; + NSMutableDictionary *roomTagsListenerByUserId; } @end @@ -67,6 +75,8 @@ - (instancetype)init lowPrioritySection = -1; sectionsCount = 0; + shrinkedSectionsBitMask = 0; + roomTagsListenerByUserId = [[NSMutableDictionary alloc] init]; // Set default data and view classes @@ -219,19 +229,19 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger { count = 1; } - else if (section == favoritesSection) + else if (section == favoritesSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_FAVORITES)) { count = favoriteCellDataArray.count; } - else if (section == conversationSection) + else if (section == conversationSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_CONVERSATIONS)) { count = conversationCellDataArray.count; } - else if (section == lowPrioritySection) + else if (section == lowPrioritySection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_LOWPRIORITY)) { count = lowPriorityCellDataArray.count; } - else if (section == invitesSection) + else if (section == invitesSection && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_INVITES)) { count = invitesCellDataArray.count; } @@ -241,7 +251,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger count++; } - if ([self isHiddenCellSection:section]) + if (count && [self isHiddenCellSection:section]) { count--; } @@ -251,43 +261,89 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame { - // add multi accounts section management + UIView *sectionHeader = nil; - if ((section == directorySection) || (section == favoritesSection) || (section == conversationSection) || (section == lowPrioritySection) || (section == invitesSection)) + if (section < sectionsCount) { - UILabel* label = [[UILabel alloc] initWithFrame:frame]; - - NSString* text = @""; + NSString* sectionTitle = @""; + NSInteger sectionBitwise = 0; + UIImageView *chevronView; if (section == directorySection) { - text = NSLocalizedStringFromTable(@"room_recents_directory", @"Vector", nil); + sectionTitle = NSLocalizedStringFromTable(@"room_recents_directory", @"Vector", nil); } else if (section == favoritesSection) { - text = NSLocalizedStringFromTable(@"room_recents_favourites", @"Vector", nil); + sectionTitle = NSLocalizedStringFromTable(@"room_recents_favourites", @"Vector", nil); + sectionBitwise = RECENTSDATASOURCE_SECTION_FAVORITES; } else if (section == conversationSection) { - text = NSLocalizedStringFromTable(@"room_recents_conversations", @"Vector", nil); + sectionTitle = NSLocalizedStringFromTable(@"room_recents_conversations", @"Vector", nil); + sectionBitwise = RECENTSDATASOURCE_SECTION_CONVERSATIONS; } else if (section == lowPrioritySection) { - text = NSLocalizedStringFromTable(@"room_recents_low_priority", @"Vector", nil); + sectionTitle = NSLocalizedStringFromTable(@"room_recents_low_priority", @"Vector", nil); + sectionBitwise = RECENTSDATASOURCE_SECTION_LOWPRIORITY; } else if (section == invitesSection) { - text = NSLocalizedStringFromTable(@"room_recents_invites", @"Vector", nil); + sectionTitle = NSLocalizedStringFromTable(@"room_recents_invites", @"Vector", nil); + sectionBitwise = RECENTSDATASOURCE_SECTION_INVITES; } - - label.text = [NSString stringWithFormat:@" %@", text]; - label.font = [UIFont boldSystemFontOfSize:15.0]; - label.backgroundColor = kVectorColorLightGrey; - return label; - } - - return [super viewForHeaderInSection:section withFrame:frame]; + sectionHeader = [[UIView alloc] initWithFrame:frame]; + sectionHeader.backgroundColor = kVectorColorLightGrey; + + 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]; + } + + return sectionHeader; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)anIndexPath @@ -319,7 +375,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N // add an imageview of the cell. // The image is a shot of the genuine cell. - // Thus, this cell has the same look as the genuine cell withourt computing it. + // Thus, this cell has the same look as the genuine cell without computing it. UIImageView* imageView = [cell viewWithTag:[cellIdentifier hash]]; if (!imageView || (imageView != self.droppingCellBackGroundView)) @@ -458,14 +514,19 @@ - (NSInteger)cellIndexPosWithRoomId:(NSString*)roomId andMatrixSession:(MXSessio - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession { NSIndexPath *indexPath = nil; - NSInteger index = NSNotFound; + NSInteger index; - if (!indexPath && (invitesSection >= 0)) + if (invitesSection >= 0) { index = [self cellIndexPosWithRoomId:roomId andMatrixSession:matrixSession within:invitesCellDataArray]; if (index != NSNotFound) { + // Check whether the invitations are shrinked + if (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_INVITES) + { + return nil; + } indexPath = [NSIndexPath indexPathForRow:index inSection:invitesSection]; } } @@ -476,6 +537,11 @@ - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSe if (index != NSNotFound) { + // Check whether the favorites are shrinked + if (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_FAVORITES) + { + return nil; + } indexPath = [NSIndexPath indexPathForRow:index inSection:favoritesSection]; } } @@ -486,6 +552,11 @@ - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSe if (index != NSNotFound) { + // Check whether the conversations are shrinked + if (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_CONVERSATIONS) + { + return nil; + } indexPath = [NSIndexPath indexPathForRow:index inSection:conversationSection]; } } @@ -496,15 +567,15 @@ - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSe if (index != NSNotFound) { + // Check whether the low priority rooms are shrinked + if (shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_LOWPRIORITY) + { + return nil; + } indexPath = [NSIndexPath indexPathForRow:index inSection:lowPrioritySection]; } } - if (!indexPath) - { - indexPath = [super cellIndexPathWithRoomId:roomId andMatrixSession:matrixSession]; - } - return indexPath; } @@ -651,6 +722,31 @@ - (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes [super dataSource:dataSource didCellChange:changes]; } +#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 - Override MXKDataSource - (void)destroy diff --git a/Vector/Utils/VectorDesignValues.h b/Vector/Utils/VectorDesignValues.h index c2ffe5a29c..215a0cacb4 100644 --- a/Vector/Utils/VectorDesignValues.h +++ b/Vector/Utils/VectorDesignValues.h @@ -29,7 +29,7 @@ extern UIColor *kVectorColorGreen; extern UIColor *kVectorColorLightGreen; extern UIColor *kVectorColorLightGrey; -extern UIColor *kVectorColorSiver; +extern UIColor *kVectorColorSilver; extern UIColor *kVectorColorOrange; #pragma mark - Vector Text Colors diff --git a/Vector/Utils/VectorDesignValues.m b/Vector/Utils/VectorDesignValues.m index daebf258fb..101946f03f 100644 --- a/Vector/Utils/VectorDesignValues.m +++ b/Vector/Utils/VectorDesignValues.m @@ -19,7 +19,7 @@ UIColor *kVectorColorGreen; UIColor *kVectorColorLightGreen; UIColor *kVectorColorLightGrey; -UIColor *kVectorColorSiver; +UIColor *kVectorColorSilver; UIColor *kVectorColorOrange; UIColor *kVectorTextColorBlack; @@ -43,7 +43,7 @@ + (void)load // Colors as defined by the design kVectorColorGreen = [UIColor colorWithRed:(98.0/255.0) green:(206.0/255.0) blue:(156.0/255.0) alpha:1.0]; kVectorColorLightGrey = [UIColor colorWithRed:(242.0 / 255.0) green:(242.0 / 255.0) blue:(242.0 / 255.0) alpha:1.0]; - kVectorColorSiver = [UIColor colorWithRed:(199.0 / 255.0) green:(199.0 / 255.0) blue:(204.0 / 255.0) alpha:1.0]; + kVectorColorSilver = [UIColor colorWithRed:(199.0 / 255.0) green:(199.0 / 255.0) blue:(204.0 / 255.0) alpha:1.0]; kVectorTextColorBlack = [UIColor colorWithRed:(60.0 / 255.0) green:(60.0 / 255.0) blue:(60.0 / 255.0) alpha:1.0]; kVectorTextColorRed = [UIColor colorWithRed:(255.0 / 255.0) green:(0.0 / 255.0) blue:(100.0 / 255.0) alpha:1.0]; diff --git a/Vector/Vector-Defaults.plist b/Vector/Vector-Defaults.plist index 70d1e1c1b1..52ae90cbb1 100644 --- a/Vector/Vector-Defaults.plist +++ b/Vector/Vector-Defaults.plist @@ -14,6 +14,8 @@ https://matrix.org homeserver matrix.org + webAppUrlDev + https://vector.im/develop apnsDeviceToken showAllEventsInRoomHistory diff --git a/Vector/Vector.entitlements b/Vector/Vector.entitlements new file mode 100644 index 0000000000..16f53850ec --- /dev/null +++ b/Vector/Vector.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:vector.im + + + diff --git a/Vector/ViewController/AuthenticationViewController.m b/Vector/ViewController/AuthenticationViewController.m index 99da581add..a488bf114c 100644 --- a/Vector/ViewController/AuthenticationViewController.m +++ b/Vector/ViewController/AuthenticationViewController.m @@ -154,8 +154,8 @@ - (IBAction)onButtonPressed:(id)sender // Check whether a request is in progress if (!self.userInteractionEnabled) { - // Cancel the current operation, and reset the UI by forcing the authType property. - self.authType = self.authType; + // Cancel the current operation + [self cancel]; } else if (self.authType == MXKAuthenticationTypeLogin) { diff --git a/Vector/ViewController/DirectoryViewController.m b/Vector/ViewController/DirectoryViewController.m index 49240c16c7..d6117251c5 100644 --- a/Vector/ViewController/DirectoryViewController.m +++ b/Vector/ViewController/DirectoryViewController.m @@ -125,7 +125,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath - (void)openRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)mxSession { dispatch_async(dispatch_get_main_queue(), ^{ - [[AppDelegate theDelegate].homeViewController selectRoomWithId:roomId inMatrixSession:mxSession]; + [[AppDelegate theDelegate].homeViewController selectRoomWithId:roomId andEventId:nil inMatrixSession:mxSession]; }); } diff --git a/Vector/ViewController/HomeViewController.h b/Vector/ViewController/HomeViewController.h index 582fb6c9f1..f22f588741 100644 --- a/Vector/ViewController/HomeViewController.h +++ b/Vector/ViewController/HomeViewController.h @@ -17,8 +17,8 @@ #import #import "SegmentedViewController.h" - -@class RoomViewController; +#import "RoomViewController.h" +#import "AuthenticationViewController.h" /** The `HomeViewController` screen is the main app screen. @@ -31,14 +31,28 @@ // 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; + /** Start displaying the screen with a user Matrix session. @@ -50,9 +64,19 @@ 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 inMatrixSession:(MXSession*)mxSession; +- (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) diff --git a/Vector/ViewController/HomeViewController.m b/Vector/ViewController/HomeViewController.m index f023a36d4e..3cebb8ff63 100644 --- a/Vector/ViewController/HomeViewController.m +++ b/Vector/ViewController/HomeViewController.m @@ -43,6 +43,18 @@ @interface HomeViewController () 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; } @end @@ -95,7 +107,19 @@ - (void)dealloc - (void)destroy { [super destroy]; - + + if (currentAlert) + { + [currentAlert dismiss:NO]; + currentAlert = nil; + } + + if (authViewControllerObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:authViewControllerObserver]; + authViewControllerObserver = nil; + } + if (roomCreationRequest) { [roomCreationRequest cancel]; @@ -244,11 +268,36 @@ - (void)viewDidLayoutSubviews - (void)showAuthenticationScreen { - [[AppDelegate theDelegate] restoreInitialDisplay:^{ - - [self performSegueWithIdentifier:@"showAuth" sender:self]; + // 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)displayWithSession:(MXSession *)mxSession @@ -274,6 +323,8 @@ - (void)addMatrixSession:(MXSession *)mxSession { [recentsDataSource addMatrixSession:mxSession]; } + + [super addMatrixSession:mxSession]; } - (void)removeMatrixSession:(MXSession *)mxSession @@ -288,14 +339,17 @@ - (void)removeMatrixSession:(MXSession *)mxSession [recentsViewController displayList:nil]; [previousRecentlistDataSource destroy]; } + + [super removeMatrixSession:mxSession]; } -- (void)selectRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession +- (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 @@ -303,6 +357,7 @@ - (void)selectRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSes } _selectedRoomId = roomId; + _selectedEventId = eventId; _selectedRoomSession = matrixSession; if (roomId && matrixSession) @@ -315,9 +370,22 @@ - (void)selectRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSes } } +- (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) @@ -352,6 +420,23 @@ - (void)setKeyboardHeight:(CGFloat)keyboardHeight [super setKeyboardHeight:keyboardHeight]; } +- (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 @@ -436,22 +521,39 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender _currentRoomViewController = (RoomViewController *)controller; - // Live timeline or timeline from a search result? - MXKRoomDataSource *roomDataSource; - if (!searchViewController.selectedEvent) + if (!_selectedRoomPreviewData) { - // LIVE: Show the room live timeline managed by MXKRoomDataSourceManager - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:_selectedRoomSession]; - roomDataSource = [roomDataSourceManager roomDataSourceForRoom:_selectedRoomId create:YES]; + // Live timeline or timeline from a search result? + MXKRoomDataSource *roomDataSource; + if (!searchViewController.selectedEvent) + { + 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]; + } + } + else + { + // Search result: Create a temp timeline from the selected event + roomDataSource = [[RoomDataSource alloc] initWithRoomId:searchViewController.selectedEvent.roomId initialEventId:searchViewController.selectedEvent.eventId andMatrixSession:searchDataSource.mxSession]; + [roomDataSource finalizeInitialization]; + } + + [_currentRoomViewController displayRoom:roomDataSource]; } else { - // Search result: Create a temp timeline from the selected event - roomDataSource = [[RoomDataSource alloc] initWithRoomId:searchViewController.selectedEvent.roomId initialEventId:searchViewController.selectedEvent.eventId andMatrixSession:searchDataSource.mxSession]; - [roomDataSource finalizeInitialization]; + [_currentRoomViewController displayRoomPreview:_selectedRoomPreviewData]; + _selectedRoomPreviewData = nil; } - - [_currentRoomViewController displayRoom:roomDataSource]; } if (self.splitViewController) @@ -479,6 +581,28 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender DirectoryViewController *directoryViewController = segue.destinationViewController; [directoryViewController displayWitDataSource:recentsDataSource.publicRoomsDirectoryDataSource]; } + 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; + } + } } // Hide back button title @@ -584,7 +708,7 @@ - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession { // Open the room - [self selectRoomWithId:roomId inMatrixSession:matrixSession]; + [self selectRoomWithId:roomId andEventId:nil inMatrixSession:matrixSession]; } #pragma mark - Actions @@ -602,35 +726,67 @@ - (void)onNewRoomPressed // Sanity check if (self.mainSession) { - createNewRoomImageView.userInteractionEnabled = NO; - - [recentsViewController startActivityIndicator]; - - // Create an empty room. - roomCreationRequest = [self.mainSession createRoom:nil - visibility:kMXRoomVisibilityPrivate - roomAlias:nil - topic:nil - success:^(MXRoom *room) { - - roomCreationRequest = nil; - [recentsViewController stopActivityIndicator]; - createNewRoomImageView.userInteractionEnabled = YES; - - [self selectRoomWithId:room.state.roomId inMatrixSession:self.mainSession]; - - } failure:^(NSError *error) { - - roomCreationRequest = nil; - [recentsViewController stopActivityIndicator]; - createNewRoomImageView.userInteractionEnabled = YES; - - NSLog(@"[RoomCreation] Create new room failed"); - - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; + // Create one room at time + if (!roomCreationRequest) + { + [recentsViewController startActivityIndicator]; + + // Create an empty room. + roomCreationRequest = [self.mainSession createRoom:nil + visibility:kMXRoomVisibilityPrivate + 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(@"[RoomCreation] 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 showInViewController:self]; + + } } } diff --git a/Vector/ViewController/RoomMemberDetailsViewController.h b/Vector/ViewController/RoomMemberDetailsViewController.h index b803824edb..32ae610553 100644 --- a/Vector/ViewController/RoomMemberDetailsViewController.h +++ b/Vector/ViewController/RoomMemberDetailsViewController.h @@ -16,12 +16,13 @@ #import -@interface RoomMemberDetailsViewController : MXKRoomMemberDetailsViewController +@interface RoomMemberDetailsViewController : MXKRoomMemberDetailsViewController @property (weak, nonatomic) IBOutlet NSLayoutConstraint *actionTableViewTopConstraint; @property (weak, nonatomic) IBOutlet UIView *memberHeaderView; @property (weak, nonatomic) IBOutlet UILabel *roomMemberNameLabel; +@property (weak, nonatomic) IBOutlet UIView *roomMemberNameLabelMask; @property (weak, nonatomic) IBOutlet UILabel *roomMemberStatusLabel; diff --git a/Vector/ViewController/RoomMemberDetailsViewController.m b/Vector/ViewController/RoomMemberDetailsViewController.m index 3f62ca356d..ce79ae97d6 100644 --- a/Vector/ViewController/RoomMemberDetailsViewController.m +++ b/Vector/ViewController/RoomMemberDetailsViewController.m @@ -74,6 +74,13 @@ - (void)viewDidLoad self.roomMemberNameLabel.textColor = kVectorTextColorBlack; self.roomMemberStatusLabel.textColor = kVectorColorGreen; + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; + [tap setNumberOfTouchesRequired:1]; + [tap setNumberOfTapsRequired:1]; + [tap setDelegate:self]; + [self.roomMemberNameLabelMask addGestureRecognizer:tap]; + self.roomMemberNameLabelMask.userInteractionEnabled = YES; + self.navigationItem.titleView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 600, 40)]; memberTitleView = [RoomMemberTitleView roomMemberTitleView]; @@ -194,24 +201,27 @@ - (void)updateMemberInfo { self.roomMemberNameLabel.text = self.mxRoomMember.displayname ? self.mxRoomMember.displayname : self.mxRoomMember.userId; - NSString* presenceText = nil; - - if (self.mxRoomMember.membership != MXMembershipJoin) + // Update member badge + MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels]; + NSInteger powerLevel = [powerLevels powerLevelOfUserWithUserID:self.mxRoomMember.userId]; + if (powerLevel >= kVectorRoomAdminLevel) { - if (self.mxRoomMember.membership == MXMembershipInvite) - { - presenceText = NSLocalizedStringFromTable(@"room_participants_invite", @"Vector", nil); - } - else if (self.mxRoomMember.membership == MXMembershipLeave) - { - presenceText = NSLocalizedStringFromTable(@"room_participants_leave", @"Vector", nil); - } - else if (self.mxRoomMember.membership == MXMembershipBan) - { - presenceText = NSLocalizedStringFromTable(@"room_participants_ban", @"Vector", nil); - } + memberTitleView.memberBadge.image = [UIImage imageNamed:@"admin_icon"]; + memberTitleView.memberBadge.hidden = NO; + } + else if (powerLevel >= kVectorRoomModeratorLevel) + { + memberTitleView.memberBadge.image = [UIImage imageNamed:@"mod_icon"]; + memberTitleView.memberBadge.hidden = NO; } - else if (self.mxRoomMember.userId) + else + { + memberTitleView.memberBadge.hidden = YES; + } + + NSString* presenceText = nil; + + if (self.mxRoomMember.userId) { MXUser *user = [self.mxRoom.mxSession userWithUserId:self.mxRoomMember.userId]; if (user) @@ -491,7 +501,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N return cell; } -#pragma mark - +#pragma mark - Action - (void)onActionButtonPressed:(id)sender { @@ -530,4 +540,24 @@ - (void)onActionButtonPressed:(id)sender } } +- (void)handleTapGesture:(UITapGestureRecognizer*)tapGestureRecognizer +{ + UIView *view = tapGestureRecognizer.view; + + if (view == self.roomMemberNameLabelMask && self.mxRoomMember.displayname) + { + if ([self.roomMemberNameLabel.text isEqualToString:self.mxRoomMember.displayname]) + { + // Display room member matrix id + self.roomMemberNameLabel.text = self.mxRoomMember.userId; + } + else + { + // Restore display name + self.roomMemberNameLabel.text = self.mxRoomMember.displayname; + } + } +} + + @end diff --git a/Vector/ViewController/RoomMemberDetailsViewController.xib b/Vector/ViewController/RoomMemberDetailsViewController.xib index bbe76ba791..da944dd711 100644 --- a/Vector/ViewController/RoomMemberDetailsViewController.xib +++ b/Vector/ViewController/RoomMemberDetailsViewController.xib @@ -11,6 +11,7 @@ + @@ -42,11 +43,21 @@ + + + + + + + + + + diff --git a/Vector/ViewController/RoomParticipantsViewController.h b/Vector/ViewController/RoomParticipantsViewController.h index e998478a26..a705695dc2 100644 --- a/Vector/ViewController/RoomParticipantsViewController.h +++ b/Vector/ViewController/RoomParticipantsViewController.h @@ -20,36 +20,43 @@ #import "SegmentedViewController.h" +@class Contact; + /** 'RoomParticipantsViewController' instance is used to edit members of the room defined by the property 'mxRoom'. - - When this property is nil, the view controller is able to handle a list of participants without room reference. + When this property is nil, the view controller is empty. */ -@interface RoomParticipantsViewController : MXKTableViewController +@interface RoomParticipantsViewController : MXKViewController { @protected /** - The matrix id of the current user (nil if the user is not a participant of the room). + Section indexes */ - NSString *userMatrixId; + NSInteger participantsSection; + NSInteger invitedSection; + NSInteger invitableSection; /** - Section indexes + The current list of joined members (Array of 'Contact' instances). */ - NSInteger searchResultSection; - NSInteger participantsSection; + NSMutableArray *actualParticipants; /** - Mutable list of participants + The current list of invited members (Array of 'Contact' instances). */ - NSMutableArray *mutableParticipants; + NSMutableArray *invitedParticipants; /** - Store MXKContact instance by matrix user id + The contact used to describe the current user (nil if the user is not a participant of the room). */ - NSMutableDictionary *mxkContactsById; + Contact *userContact; } +@property (weak, nonatomic) IBOutlet UITableView *tableView; +@property (weak, nonatomic) IBOutlet UIView *searchBarHeader; +@property (weak, nonatomic) IBOutlet UISearchBar *searchBarView; +@property (weak, nonatomic) IBOutlet UIView *searchBarHeaderBorder; + /** A matrix room (nil by default). */ @@ -65,14 +72,5 @@ */ @property (nonatomic) SegmentedViewController *segmentedViewController; - -/** - Customize the UITableViewCell before rendering it. - - @param contactCell the cell to customize. - @param indexPath path of the cell in the tableview. - */ -- (void)customizeContactCell:(ContactTableViewCell*)contactCell atIndexPath:(NSIndexPath *)indexPath; - @end diff --git a/Vector/ViewController/RoomParticipantsViewController.m b/Vector/ViewController/RoomParticipantsViewController.m index 46b93e1303..82d98f015e 100644 --- a/Vector/ViewController/RoomParticipantsViewController.m +++ b/Vector/ViewController/RoomParticipantsViewController.m @@ -30,19 +30,28 @@ @interface RoomParticipantsViewController () { - // Add participants section - MXKTableViewCellWithSearchBar *addParticipantsSearchBarCell; - NSString *addParticipantsSearchText; + // Array used to sort participants and invited members + NSMutableArray *sortedParticipantsAdmin; + NSMutableArray *sortedParticipants; + NSMutableArray *sortedInvitedParticipantsAdmin; + NSMutableArray *sortedInvitedParticipants; + // Search session + NSString *currentSearchText; UIView* searchBarSeparator; - // Search result section - NSMutableArray *filteredParticipants; + // Search results + NSMutableArray *invitableContacts; + NSMutableArray *filteredActualParticipants; + NSMutableArray *filteredInvitedParticipants; + + // Contact instances by matrix user id, or room 3pid invite token. + NSMutableDictionary *contactsById; MXKAlert *currentAlert; // Mask view while processing a request - UIActivityIndicatorView * pendingMaskSpinnerView; + UIActivityIndicatorView *pendingMaskSpinnerView; // The members events listener. id membersListener; @@ -57,17 +66,6 @@ @interface RoomParticipantsViewController () @implementation RoomParticipantsViewController -- (void)setNavBarButtons -{ - // this viewController can be displayed - // 1- with a "standard" push mode - // 2- within a segmentedViewController i.e. inside another viewcontroller - // so, we need to use the parent controller when it is required. - UIViewController* topViewController = (self.parentViewController) ? self.parentViewController : self; - topViewController.navigationItem.rightBarButtonItem = nil; - topViewController.navigationItem.leftBarButtonItem = nil; -} - - (void)viewDidLoad { [super viewDidLoad]; @@ -87,31 +85,20 @@ - (void)viewDidLoad [self addMatrixSession:mxSession]; } - addParticipantsSearchBarCell = [[MXKTableViewCellWithSearchBar alloc] init]; - addParticipantsSearchBarCell.contentView.backgroundColor = [UIColor whiteColor]; - addParticipantsSearchBarCell.mxkSearchBar.searchBarStyle = UISearchBarStyleMinimal; - addParticipantsSearchBarCell.mxkSearchBar.returnKeyType = UIReturnKeyDone; - addParticipantsSearchBarCell.mxkSearchBar.autocapitalizationType = UITextAutocapitalizationTypeNone; - addParticipantsSearchBarCell.mxkSearchBar.delegate = self; - addParticipantsSearchBarCell.mxkSearchBar.placeholder = NSLocalizedStringFromTable(@"room_participants_invite_another_user", @"Vector", nil); - [self refreshSearchBarItemsColor:addParticipantsSearchBarCell.mxkSearchBar]; - _isAddParticipantSearchBarEditing = NO; - if (! mutableParticipants) - { - mutableParticipants = [NSMutableArray array]; - } + _searchBarView.placeholder = NSLocalizedStringFromTable(@"room_participants_invite_another_user", @"Vector", nil); + [self refreshSearchBarItemsColor:_searchBarView]; + + _searchBarHeaderBorder.backgroundColor = kVectorColorSilver; + + // Search bar header is hidden when no room is provided + _searchBarHeader.hidden = (self.mxRoom == nil); - if (! mxkContactsById) - { - mxkContactsById = [NSMutableDictionary dictionary]; - } - - // ensure that the separator line is not displayed - self.tableView.separatorColor = [UIColor clearColor]; - self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; [self setNavBarButtons]; + + // Hide line separators of empty cells + self.tableView.tableFooterView = [[UIView alloc] init]; } // this method is called when the viewcontroller is displayed inside another one. @@ -143,11 +130,20 @@ - (void)destroy _mxRoom = nil; - addParticipantsSearchBarCell = nil; - filteredParticipants = nil; - mxkContactsById = nil; + sortedParticipantsAdmin = nil; + sortedParticipants = nil; + sortedInvitedParticipantsAdmin = nil; + sortedInvitedParticipants = nil; - mutableParticipants = nil; + invitableContacts = nil; + filteredActualParticipants = nil; + filteredInvitedParticipants = nil; + + contactsById = nil; + + actualParticipants = nil; + invitedParticipants = nil; + userContact = nil; if (currentAlert) { @@ -185,16 +181,16 @@ - (void)viewWillDisappear:(BOOL)animated } // cancel any pending search - if (addParticipantsSearchBarCell.mxkSearchBar) - { - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; - } + [self searchBarCancelButtonClicked:_searchBarView]; } #pragma mark - - (void)setMxRoom:(MXRoom *)mxRoom { + // Cancel any pending search + [self searchBarCancelButtonClicked:_searchBarView]; + // Remove the previous listener if (leaveRoomNotificationObserver) { @@ -203,24 +199,25 @@ - (void)setMxRoom:(MXRoom *)mxRoom } if (membersListener) { - [self.mxRoom.liveTimeline removeListener:membersListener]; + [_mxRoom.liveTimeline removeListener:membersListener]; + membersListener = nil; } _mxRoom = mxRoom; - // Refresh displayed participants from the current room members - [self refreshParticipantsFromRoomMembers]; + // Search bar header is hidden when no room is provided + _searchBarHeader.hidden = (self.mxRoom == nil); - if (mxRoom) + if (_mxRoom) { // Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room. leaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { // Check whether the user will leave the room related to the displayed participants - if (notif.object == self.mxRoom.mxSession) + if (notif.object == _mxRoom.mxSession) { NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey]; - if (roomId && [roomId isEqualToString:self.mxRoom.state.roomId]) + if (roomId && [roomId isEqualToString:_mxRoom.state.roomId]) { // We remove the current view controller. [self withdrawViewControllerAnimated:YES completion:nil]; @@ -230,7 +227,7 @@ - (void)setMxRoom:(MXRoom *)mxRoom // Register a listener for events that concern room members NSArray *mxMembersEvents = @[kMXEventTypeStringRoomMember, kMXEventTypeStringRoomThirdPartyInvite, kMXEventTypeStringRoomPowerLevels]; - membersListener = [self.mxRoom.liveTimeline listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) { + membersListener = [_mxRoom.liveTimeline listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) { // Consider only live event if (direction == MXTimelineDirectionForwards) @@ -246,7 +243,20 @@ - (void)setMxRoom:(MXRoom *)mxRoom MXRoomMember *mxMember = [self.mxRoom.state memberWithUserId:event.stateKey]; if (mxMember) { - [self addRoomMemberToParticipants:mxMember]; + // Remove previous occurrence of this member (if any) + [self removeParticipantByKey:mxMember.userId]; + + // If any, remove 3pid invite corresponding to this room member + if (mxMember.thirdPartyInviteToken) + { + [self removeParticipantByKey:mxMember.thirdPartyInviteToken]; + } + + [self handleRoomMember:mxMember]; + + [self finalizeParticipantsList]; + + [self.tableView reloadData]; } } @@ -258,51 +268,69 @@ - (void)setMxRoom:(MXRoom *)mxRoom if (thirdPartyInvite) { [self addRoomThirdPartyInviteToParticipants:thirdPartyInvite]; + + [self finalizeParticipantsList]; + + [self.tableView reloadData]; } break; } case MXEventTypeRoomPowerLevels: { [self refreshParticipantsFromRoomMembers]; + + [self.tableView reloadData]; break; } default: break; } - - // Refresh participants display (if visible) - if (participantsSection != -1) - { - NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange (participantsSection, 1)]; - [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; - } } }]; } + // Refresh the members list. + [self refreshParticipantsFromRoomMembers]; + [self.tableView reloadData]; } - (void)setIsAddParticipantSearchBarEditing:(BOOL)isAddParticipantsSearchBarEditing { - _isAddParticipantSearchBarEditing = isAddParticipantsSearchBarEditing; - - // Switch the display between search result and participants list - [self.tableView reloadData]; + if (_isAddParticipantSearchBarEditing != isAddParticipantsSearchBarEditing) + { + _isAddParticipantSearchBarEditing = isAddParticipantsSearchBarEditing; + + // Switch the display between search result and participants list + [self.tableView reloadData]; + } } #pragma mark - Internals +- (void)setNavBarButtons +{ + // this viewController can be displayed + // 1- with a "standard" push mode + // 2- within a segmentedViewController i.e. inside another viewcontroller + // so, we need to use the parent controller when it is required. + UIViewController* topViewController = (self.parentViewController) ? self.parentViewController : self; + topViewController.navigationItem.rightBarButtonItem = nil; + topViewController.navigationItem.leftBarButtonItem = nil; +} + - (void)refreshParticipantsFromRoomMembers { - // Flush existing participants list - mutableParticipants = [NSMutableArray array]; - mxkContactsById = [NSMutableDictionary dictionary]; - userMatrixId = nil; + sortedParticipants = [NSMutableArray array]; + sortedParticipantsAdmin = [NSMutableArray array]; + sortedInvitedParticipants = [NSMutableArray array]; + sortedInvitedParticipantsAdmin = [NSMutableArray array]; + userContact = nil; if (self.mxRoom) { + // Retrieve the current members from the room state NSArray *members = self.mxRoom.state.members; NSString *userId = self.mxRoom.mxSession.myUser.userId; NSArray *roomThirdPartyInvites = self.mxRoom.state.thirdPartyInvites; @@ -315,12 +343,24 @@ - (void)refreshParticipantsFromRoomMembers if (mxMember.membership == MXMembershipJoin || mxMember.membership == MXMembershipInvite) { // The user is in this room - userMatrixId = userId; + + // Check whether user is admin + MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels]; + BOOL isAdmin = ([powerLevels powerLevelOfUserWithUserID:userId] >= kVectorRoomAdminLevel); + + NSString *displayName = NSLocalizedStringFromTable(@"you", @"Vector", nil); + if (isAdmin) + { + displayName = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_participants_admin_name", @"Vector", nil), displayName]; + } + + userContact = [[Contact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:userId]; + userContact.mxMember = [self.mxRoom.state memberWithUserId:userId]; } } else { - [self addRoomMemberToParticipants:mxMember]; + [self handleRoomMember:mxMember]; } } @@ -328,25 +368,19 @@ - (void)refreshParticipantsFromRoomMembers { [self addRoomThirdPartyInviteToParticipants:roomThirdPartyInvite]; } + + [self finalizeParticipantsList]; } } -- (void)addRoomMemberToParticipants:(MXRoomMember*)mxMember +- (void)handleRoomMember:(MXRoomMember*)mxMember { - // Remove previous occurrence of this member (if any) - [self removeParticipantByKey:mxMember.userId]; - - // If any, remove 3pid invite corresponding to this room member - if (mxMember.thirdPartyInviteToken) - { - [self removeParticipantByKey:mxMember.thirdPartyInviteToken]; - } - // Add this member after checking his status if (mxMember.membership == MXMembershipJoin || mxMember.membership == MXMembershipInvite) { // Check whether this member is admin - BOOL isAdmin = ([self.mxRoom.state memberNormalizedPowerLevel:mxMember.userId] == 1); + MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels]; + BOOL isAdmin = ([powerLevels powerLevelOfUserWithUserID:mxMember.userId] >= kVectorRoomAdminLevel); // Prepare the display name of this member NSString *displayName = mxMember.displayname; @@ -363,6 +397,7 @@ - (void)addRoomMemberToParticipants:(MXRoomMember*)mxMember displayName = mxMember.userId; } } + if (isAdmin) { displayName = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_participants_admin_name", @"Vector", nil), displayName]; @@ -371,9 +406,29 @@ - (void)addRoomMemberToParticipants:(MXRoomMember*)mxMember // Create the contact related to this member Contact *contact = [[Contact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:mxMember.userId]; contact.mxMember = mxMember; - [mxkContactsById setObject:contact forKey:mxMember.userId]; - - [self addContactToParticipants:contact withKey:mxMember.userId isAdmin:isAdmin]; + + if (isAdmin) + { + if (mxMember.membership == MXMembershipInvite) + { + [sortedInvitedParticipantsAdmin addObject:contact]; + } + else + { + [sortedParticipantsAdmin addObject:contact]; + } + } + else + { + if (mxMember.membership == MXMembershipInvite) + { + [sortedInvitedParticipants addObject:contact]; + } + else + { + [sortedParticipants addObject:contact]; + } + } } } @@ -384,100 +439,148 @@ - (void)addRoomThirdPartyInviteToParticipants:(MXRoomThirdPartyInvite*)roomThird { Contact *contact = [[Contact alloc] initMatrixContactWithDisplayName:roomThirdPartyInvite.displayname andMatrixID:nil]; contact.isThirdPartyInvite = YES; - mxkContactsById[roomThirdPartyInvite.token] = contact; + contact.mxThirdPartyInvite = roomThirdPartyInvite; - [self addContactToParticipants:contact withKey:roomThirdPartyInvite.token isAdmin:NO]; + [sortedInvitedParticipants addObject:contact]; } } -- (void)addContactToParticipants:(Contact*)theContact withKey:(NSString*)key isAdmin:(BOOL)isAdmin +// key is a room member user id or a room 3pid invite token +- (void)removeParticipantByKey:(NSString*)key { - // Add this participant (admin is in first position, the other are sorted in alphabetical order by trimming special character ('@', '_'...). - NSUInteger index = 0; - NSCharacterSet *specialCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"_!~`@#$%^&*-+();:={}[],.<>?\\/\"\'"]; - NSString *trimmedDisplayName = [theContact.displayName stringByTrimmingCharactersInSet:specialCharacterSet]; - if (isAdmin) + NSUInteger index; + + if (sortedParticipantsAdmin.count) { - // Check whether there is other admin - for (NSString *userId in mutableParticipants) + for (index = 0; index < sortedParticipantsAdmin.count; index++) { - if ([self.mxRoom.state memberNormalizedPowerLevel:userId] == 1) + Contact *contact = sortedParticipantsAdmin[index]; + + if (contact.mxMember && [contact.mxMember.userId isEqualToString:key]) { - Contact *contact = [mxkContactsById objectForKey:userId]; - - // Sort admin in alphabetical order (skip symbols before comparing) - NSString *trimmedContactName = [contact.displayName stringByTrimmingCharactersInSet:specialCharacterSet]; - if (!trimmedContactName.length) - { - if (trimmedDisplayName.length || [theContact.displayName compare:contact.displayName options:NSCaseInsensitiveSearch] != NSOrderedDescending) - { - break; - } - } - else if (trimmedDisplayName.length && [trimmedDisplayName compare:trimmedContactName options:NSCaseInsensitiveSearch] != NSOrderedDescending) - { - break; - } - - index++; + [sortedParticipantsAdmin removeObjectAtIndex:index]; + return; } } } - else + + if (sortedParticipants.count) { - for (NSString *userId in mutableParticipants) + for (index = 0; index < sortedParticipants.count; index++) { - // Pass admin(s) - if ([self.mxRoom.state memberNormalizedPowerLevel:userId] == 1) + Contact *contact = sortedParticipants[index]; + + if (contact.mxMember && [contact.mxMember.userId isEqualToString:key]) { - index++; + [sortedParticipants removeObjectAtIndex:index]; + return; } - else + } + } + + if (sortedInvitedParticipantsAdmin.count) + { + for (index = 0; index < sortedInvitedParticipantsAdmin.count; index++) + { + Contact *contact = sortedInvitedParticipantsAdmin[index]; + + if (contact.mxMember && [contact.mxMember.userId isEqualToString:key]) { - Contact *contact = [mxkContactsById objectForKey:userId]; - - // Sort in alphabetical order (skip symbols before comparing) - NSString *trimmedContactName = [contact.displayName stringByTrimmingCharactersInSet:specialCharacterSet]; - if (!trimmedContactName.length) - { - if (trimmedDisplayName.length || [theContact.displayName compare:contact.displayName options:NSCaseInsensitiveSearch] != NSOrderedDescending) - { - break; - } - } - else if (trimmedDisplayName.length && [trimmedDisplayName compare:trimmedContactName options:NSCaseInsensitiveSearch] != NSOrderedDescending) - { - break; - } - - index++; + [sortedInvitedParticipantsAdmin removeObjectAtIndex:index]; + return; + } + } + } + + if (sortedInvitedParticipants.count) + { + for (index = 0; index < sortedInvitedParticipants.count; index++) + { + Contact *contact = sortedInvitedParticipants[index]; + + if (contact.mxMember && [contact.mxMember.userId isEqualToString:key]) + { + [sortedInvitedParticipants removeObjectAtIndex:index]; + return; + } + + if (contact.mxThirdPartyInvite && [contact.mxThirdPartyInvite.token isEqualToString:key]) + { + [sortedInvitedParticipants removeObjectAtIndex:index]; + return; } } } - - // Add this participant - [mutableParticipants insertObject:key atIndex:index]; } -// key is a room member user id or a room 3pid invite token -- (void)removeParticipantByKey:(NSString*)key +- (void)finalizeParticipantsList { - if (mutableParticipants.count) + // Sort contacts in alphabetical order (Use sortingDisplayName in which symbols are skipped) + NSComparator comparator = ^NSComparisonResult(Contact *contact1, Contact *contact2) { + + if (contact1.sortingDisplayName.length && contact2.sortingDisplayName.length) + { + return [contact1.sortingDisplayName compare:contact2.sortingDisplayName options:NSCaseInsensitiveSearch]; + } + else if (contact1.sortingDisplayName.length) + { + return NSOrderedAscending; + } + else if (contact2.sortingDisplayName.length) + { + return NSOrderedDescending; + } + + return [contact1.displayName compare:contact2.displayName options:NSCaseInsensitiveSearch]; + + }; + + // Sort each participants list in alphabetical order + [sortedParticipantsAdmin sortUsingComparator:comparator]; + [sortedParticipants sortUsingComparator:comparator]; + [sortedInvitedParticipantsAdmin sortUsingComparator:comparator]; + [sortedInvitedParticipants sortUsingComparator:comparator]; + + // Report sorted lists in the displayed participants list + actualParticipants = [NSMutableArray array]; + [actualParticipants addObjectsFromArray:sortedParticipantsAdmin]; + [actualParticipants addObjectsFromArray:sortedParticipants]; + + invitedParticipants = [NSMutableArray array]; + [invitedParticipants addObjectsFromArray:sortedInvitedParticipantsAdmin]; + [invitedParticipants addObjectsFromArray:sortedInvitedParticipants]; + + // Refer all used contacts in only one dictionary. + contactsById = [NSMutableDictionary dictionary]; + for (Contact *contact in actualParticipants) { - NSUInteger index = [mutableParticipants indexOfObject:key]; - if (index != NSNotFound) + [contactsById setObject:contact forKey:contact.mxMember.userId]; + } + for (Contact *contact in invitedParticipants) + { + if (contact.mxMember) { - [mxkContactsById removeObjectForKey:key]; - [mutableParticipants removeObjectAtIndex:index]; + [contactsById setObject:contact forKey:contact.mxMember.userId]; } + else if (contact.mxThirdPartyInvite) + { + [contactsById setObject:contact forKey:contact.mxThirdPartyInvite.token]; + } + } + if (userContact) + { + [contactsById setObject:userContact forKey:userContact.mxMember.userId]; } } - (void)addPendingActionMask { + // Remove potential existing mask + [self removePendingActionMask]; + // Add a spinner above the tableview to avoid that the user tap on any other button pendingMaskSpinnerView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; - pendingMaskSpinnerView.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.5]; + pendingMaskSpinnerView.backgroundColor = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:0.5]; pendingMaskSpinnerView.frame = self.tableView.frame; pendingMaskSpinnerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin; @@ -486,6 +589,16 @@ - (void)addPendingActionMask // animate it [pendingMaskSpinnerView startAnimating]; + + // Show the spinner after a delay so that if it is removed in a short future, + // it is not displayed to the end user. + pendingMaskSpinnerView.alpha = 0; + [UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + + pendingMaskSpinnerView.alpha = 1; + + } completion:^(BOOL finished) { + }]; } - (void)removePendingActionMask @@ -503,15 +616,33 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { NSInteger count = 0; - searchResultSection = participantsSection = -1; + invitableSection = participantsSection = invitedSection = -1; if (_isAddParticipantSearchBarEditing) { - searchResultSection = count++; + invitableSection = count++; + + if (filteredActualParticipants.count) + { + participantsSection = count++; + } + + if (filteredInvitedParticipants.count) + { + invitedSection = count++; + } } else { - participantsSection = count++; + if (userContact || actualParticipants.count) + { + participantsSection = count++; + } + + if (invitedParticipants.count) + { + invitedSection = count++; + } } return count; @@ -521,141 +652,126 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger { NSInteger count = 0; - if (section == searchResultSection) + if (section == invitableSection) { - count = filteredParticipants.count; + count = invitableContacts.count; } else if (section == participantsSection) { - count = mutableParticipants.count; - if (userMatrixId) + if (_isAddParticipantSearchBarEditing) + { + count = filteredActualParticipants.count; + } + else + { + count = actualParticipants.count; + if (userContact) + { + count++; + } + } + } + else if (section == invitedSection) + { + if (_isAddParticipantSearchBarEditing) + { + count = filteredInvitedParticipants.count; + } + else { - count++; + count = invitedParticipants.count; } } return count; } -- (void)customizeContactCell:(ContactTableViewCell*)contactCell atIndexPath:(NSIndexPath*) indexPath -{ - // TODO by the inherited class -} - - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - UITableViewCell *cell = nil; + ContactTableViewCell* participantCell = [tableView dequeueReusableCellWithIdentifier:[ContactTableViewCell defaultReuseIdentifier]]; - if ((indexPath.section == searchResultSection) || (indexPath.section == participantsSection)) + if (!participantCell) { - ContactTableViewCell* participantCell = [tableView dequeueReusableCellWithIdentifier:[ContactTableViewCell defaultReuseIdentifier]]; - - if (!participantCell) - { - participantCell = [[ContactTableViewCell alloc] init]; - } - else - { - // Restore default values - participantCell.accessoryView = nil; - participantCell.contentView.alpha = 1; - participantCell.userInteractionEnabled = YES; - } - - participantCell.mxRoom = self.mxRoom; + participantCell = [[ContactTableViewCell alloc] init]; + } + else + { + // Restore default values + participantCell.accessoryView = nil; + participantCell.contentView.alpha = 1; + participantCell.userInteractionEnabled = YES; - Contact *contact = nil; + participantCell.thumbnailBadgeView.hidden = YES; + } + + participantCell.mxRoom = self.mxRoom; + + Contact *contact = nil; + + // oneself dedicated cell + if ((indexPath.section == participantsSection && userContact && indexPath.row == 0) && !_isAddParticipantSearchBarEditing) + { + contact = userContact; - // oneself dedicated cell - if ((indexPath.section == participantsSection && userMatrixId && indexPath.row == 0)) + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + else if (indexPath.section == invitableSection) + { + if (indexPath.row < invitableContacts.count) { - contact = [mxkContactsById objectForKey:userMatrixId]; + contact = invitableContacts[indexPath.row]; - if (!contact) - { - // Check whether user is admin - BOOL isAdmin = ([self.mxRoom.state memberNormalizedPowerLevel:userMatrixId] == 1); - - NSString *displayName = NSLocalizedStringFromTable(@"you", @"Vector", nil); - if (isAdmin) - { - displayName = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_participants_admin_name", @"Vector", nil), displayName]; - } - - contact = [[Contact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:userMatrixId]; - contact.mxMember = [self.mxRoom.state memberWithUserId:userMatrixId]; - [mxkContactsById setObject:contact forKey:userMatrixId]; - } - } - else if (indexPath.section == searchResultSection) - { - contact = filteredParticipants[indexPath.row]; + participantCell.selectionStyle = UITableViewCellSelectionStyleDefault; } - else + } + else + { + NSInteger index = indexPath.row; + NSArray *participants; + + if (indexPath.section == participantsSection) { - NSInteger index = indexPath.row; - - if (userMatrixId) + if (_isAddParticipantSearchBarEditing) { - index --; + participants = filteredActualParticipants; } - - if (index < mutableParticipants.count) + else { - NSString *userId = mutableParticipants[index]; - contact = [mxkContactsById objectForKey:userId]; + participants = actualParticipants; - if (!contact) + if (userContact) { - // Create this missing contact - // Look for the corresponding MXUser - NSArray *sessions = self.mxSessions; - MXUser *mxUser; - for (MXSession *session in sessions) - { - mxUser = [session userWithUserId:userId]; - if (mxUser) - { - contact = [[Contact alloc] initMatrixContactWithDisplayName:((mxUser.displayname.length > 0) ? mxUser.displayname : userId) andMatrixID:userId]; - contact.mxMember = [self.mxRoom.state memberWithUserId:userId]; - break; - } - } - - if (contact) - { - [mxkContactsById setObject:contact forKey:userId]; - } - + index --; } } } - - if (indexPath.section == searchResultSection) - { - participantCell.selectionStyle = UITableViewCellSelectionStyleDefault; - participantCell.bottomLineSeparator.hidden = ((indexPath.row+1) != filteredParticipants.count); - } else { - participantCell.selectionStyle = UITableViewCellSelectionStyleNone; - - if (userMatrixId) + if (_isAddParticipantSearchBarEditing) { - participantCell.bottomLineSeparator.hidden = ((indexPath.row) != mutableParticipants.count); + participants = filteredInvitedParticipants; } else { - participantCell.bottomLineSeparator.hidden = ((indexPath.row+1) != mutableParticipants.count); + participants = invitedParticipants; } } - [self customizeContactCell:participantCell atIndexPath:indexPath]; + if (index < participants.count) + { + contact = participants[index]; + } + + participantCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + if (contact) + { [participantCell render:contact]; - + // The search displays contacts to invite. Add a plus icon to the cell // in order to make it more understandable for the end user - if (indexPath.section == searchResultSection) + if (indexPath.section == invitableSection) { if (indexPath.row == 0) { @@ -678,11 +794,25 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N participantCell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"plus_icon"]]; } } - - cell = participantCell; + else if (contact.mxMember) + { + // Update member badge + MXRoomPowerLevels *powerLevels = [self.mxRoom.state powerLevels]; + NSInteger powerLevel = [powerLevels powerLevelOfUserWithUserID:contact.mxMember.userId]; + if (powerLevel >= kVectorRoomAdminLevel) + { + participantCell.thumbnailBadgeView.image = [UIImage imageNamed:@"admin_icon"]; + participantCell.thumbnailBadgeView.hidden = NO; + } + else if (powerLevel >= kVectorRoomModeratorLevel) + { + participantCell.thumbnailBadgeView.image = [UIImage imageNamed:@"mod_icon"]; + participantCell.thumbnailBadgeView.hidden = NO; + } + } } - return cell; + return participantCell; } - (void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath @@ -694,12 +824,44 @@ - (void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEdi - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - return addParticipantsSearchBarCell.contentView.frame.size.height; + if (section == invitedSection) + { + return 30.0; + } + else if (section == participantsSection && _isAddParticipantSearchBarEditing) + { + return 1; + } + return 0; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { - return addParticipantsSearchBarCell.contentView; + UIView* sectionHeader; + + if (section == invitedSection) + { + sectionHeader = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, 30)]; + sectionHeader.backgroundColor = kVectorColorLightGrey; + + CGRect frame = sectionHeader.frame; + frame.origin.x = 20; + frame.origin.y = 5; + frame.size.width = 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 = NSLocalizedStringFromTable(@"room_participants_invited_section", @"Vector", nil); + [sectionHeader addSubview:headerLabel]; + } + else if (section == participantsSection && _isAddParticipantSearchBarEditing) + { + sectionHeader = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, 1)]; + + sectionHeader.backgroundColor = [UIColor blackColor]; + } + return sectionHeader; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath @@ -709,158 +871,166 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + // Sanity check + if (!self.mxRoom) + { + return; + } + NSInteger row = indexPath.row; - if (indexPath.section == searchResultSection) + if (indexPath.section == invitableSection) { if (row == 0) { // This is the text entered by the user // Try to invite what he typed - MXKContact *contact = filteredParticipants[row]; + MXKContact *contact = invitableContacts[row]; - // Invite this user if a room is defined - if (self.mxRoom) + // Invite this user + NSString *participantId = contact.displayName; + + // Is it an email or a Matrix user ID? + if ([MXTools isEmailAddress:participantId]) { - NSString *participantId = contact.displayName; - - // Is it an email or a Matrix user ID? - if ([MXTools isEmailAddress:participantId]) - { - [self addPendingActionMask]; - [self.mxRoom inviteUserByEmail:participantId success:^{ - - [self removePendingActionMask]; - - // Refresh display by leaving search session - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; - - } failure:^(NSError *error) { - - [self removePendingActionMask]; - - NSLog(@"[RoomParticipantsVC] Invite be email %@ failed", participantId); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; - } - else - { - [self addPendingActionMask]; - [self.mxRoom inviteUser:participantId success:^{ - - [self removePendingActionMask]; - - // Refresh display by leaving search session - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; - - } failure:^(NSError *error) { - - [self removePendingActionMask]; - - NSLog(@"[RoomParticipantsVC] Invite %@ failed", participantId); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; - } + [self addPendingActionMask]; + [self.mxRoom inviteUserByEmail:participantId success:^{ + + [self removePendingActionMask]; + + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:_searchBarView]; + + } failure:^(NSError *error) { + + [self removePendingActionMask]; + + NSLog(@"[RoomParticipantsVC] Invite be email %@ failed", participantId); + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + else + { + [self addPendingActionMask]; + [self.mxRoom inviteUser:participantId success:^{ + + [self removePendingActionMask]; + + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:_searchBarView]; + + } failure:^(NSError *error) { + + [self removePendingActionMask]; + + NSLog(@"[RoomParticipantsVC] Invite %@ failed", participantId); + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; } - // TODO handle here the case where self.mxRoom is undefined. } - else if (row < filteredParticipants.count) + else if (row < invitableContacts.count) { - MXKContact *contact = filteredParticipants[row]; + MXKContact *contact = invitableContacts[row]; NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count) { NSString *participantId = identifiers.firstObject; - - // Handle a mapping contact by userId for selected participants - [mxkContactsById setObject:contact forKey:participantId]; // Invite this user if a room is defined - if (self.mxRoom) + [self addPendingActionMask]; + [self.mxRoom inviteUser:participantId success:^{ + + [self removePendingActionMask]; + + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:_searchBarView]; + + } failure:^(NSError *error) { + + [self removePendingActionMask]; + + NSLog(@"[RoomParticipantsVC] Invite %@ failed", participantId); + // Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + else + { + // This is a local email contact + NSString *emailAddress = contact.displayName; + + // Sanity check + if ([MXTools isEmailAddress:emailAddress]) { + // Invite this user if a room is defined [self addPendingActionMask]; - [self.mxRoom inviteUser:participantId success:^{ - + [self.mxRoom inviteUserByEmail:emailAddress success:^{ + [self removePendingActionMask]; // Refresh display by leaving search session - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; + [self searchBarCancelButtonClicked:_searchBarView]; } failure:^(NSError *error) { [self removePendingActionMask]; - NSLog(@"[RoomParticipantsVC] Invite %@ failed", participantId); + NSLog(@"[RoomParticipantsVC] Invite be email %@ failed", emailAddress); // Alert user [[AppDelegate theDelegate] showErrorAsAlert:error]; }]; } - else - { - // Update here the mutable list of participants - [mutableParticipants addObject:participantId]; - // Refresh display by leaving search session - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; - } - } - else - { - // This is a local email contact - NSString *emailAddress = contact.displayName; - - // Invite this user if a room is defined - if (self.mxRoom) - { - // Sanity check - if ([MXTools isEmailAddress:emailAddress]) - { - [self addPendingActionMask]; - [self.mxRoom inviteUserByEmail:emailAddress success:^{ - - [self removePendingActionMask]; - - // Refresh display by leaving search session - [self searchBarCancelButtonClicked:addParticipantsSearchBarCell.mxkSearchBar]; - - } failure:^(NSError *error) { - - [self removePendingActionMask]; - - NSLog(@"[RoomParticipantsVC] Invite be email %@ failed", emailAddress); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - }]; - } - } - // TODO handle here the case where self.mxRoom is undefined. } } } - else if (indexPath.section == participantsSection) + else { Contact *contact; // oneself dedicated cell - if (userMatrixId && indexPath.row == 0) + if ((indexPath.section == participantsSection && userContact && indexPath.row == 0) && !_isAddParticipantSearchBarEditing) { - contact = [mxkContactsById objectForKey:userMatrixId]; + contact = userContact; } else { NSInteger index = indexPath.row; + NSArray *participants; - if (userMatrixId) + if (indexPath.section == participantsSection) + { + if (_isAddParticipantSearchBarEditing) + { + participants = filteredActualParticipants; + } + else + { + participants = actualParticipants; + + if (userContact) + { + index --; + } + } + } + else { - index --; + if (_isAddParticipantSearchBarEditing) + { + participants = filteredInvitedParticipants; + } + else + { + participants = invitedParticipants; + } } - if (index < mutableParticipants.count) + if (index < participants.count) { - NSString *userId = mutableParticipants[index]; - contact = [mxkContactsById objectForKey:userId]; + contact = participants[index]; } } @@ -894,8 +1064,8 @@ - (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NS { NSMutableArray* actions = [[NSMutableArray alloc] init]; - // add the swipe to delete on search and participants section - if (indexPath.section == participantsSection) + // add the swipe to delete only on participants sections + if (indexPath.section == participantsSection || indexPath.section == invitedSection) { NSString* title = @" "; @@ -920,7 +1090,7 @@ - (void)onDeleteAt:(NSIndexPath*)path NSUInteger section = path.section; NSUInteger row = path.row; - if (section == participantsSection) + if (section == participantsSection || section == invitedSection) { __weak typeof(self) weakSelf = self; @@ -930,7 +1100,7 @@ - (void)onDeleteAt:(NSIndexPath*)path currentAlert = nil; } - if (userMatrixId && (0 == row)) + if (section == participantsSection && userContact && (0 == row)) { // Leave ? currentAlert = [[MXKAlert alloc] initWithTitle:NSLocalizedStringFromTable(@"room_participants_leave_prompt_title", @"Vector", nil) @@ -981,15 +1151,26 @@ - (void)onDeleteAt:(NSIndexPath*)path } else { - if (userMatrixId) + NSMutableArray *participants; + + if (section == participantsSection) { - row --; + participants = actualParticipants; + + if (userContact) + { + row --; + } + } + else + { + participants = invitedParticipants; } - if (row < mutableParticipants.count) + if (row < participants.count) { - NSString *memberUserId = mutableParticipants[row]; - MXKContact *contact = [mxkContactsById objectForKey:memberUserId]; + Contact *contact = participants[row]; + NSString *memberUserId = contact.mxMember ? contact.mxMember.userId : contact.mxThirdPartyInvite.token; // Kick ? NSString *promptMsg = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_participants_remove_prompt_msg", @"Vector", nil), (contact ? contact.displayName : memberUserId)]; @@ -1020,8 +1201,7 @@ - (void)onDeleteAt:(NSIndexPath*)path [strongSelf removePendingActionMask]; - [strongSelf->mxkContactsById removeObjectForKey:memberUserId]; - [strongSelf->mutableParticipants removeObjectAtIndex:row]; + [participants removeObjectAtIndex:row]; // Refresh display [strongSelf.tableView reloadData]; @@ -1118,13 +1298,16 @@ - (void)refreshSearchBarItemsColor:(UISearchBar *)searchBar - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { - NSInteger previousFilteredCount = filteredParticipants.count; - + // Update search results. NSMutableArray *contacts; + NSMutableArray *participantsArray; + NSMutableArray *invitedParticipantsArray; - if (addParticipantsSearchText.length && [searchText hasPrefix:addParticipantsSearchText]) + if (currentSearchText.length && [searchText hasPrefix:currentSearchText]) { - contacts = filteredParticipants; + contacts = invitableContacts; + participantsArray = filteredActualParticipants; + invitedParticipantsArray = filteredInvitedParticipants; } else { @@ -1150,59 +1333,68 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { for (NSString *userId in identifiers) { - if (!mutableParticipants || [mutableParticipants indexOfObject:userId] == NSNotFound) + if ([contactsById objectForKey:userId] == nil) { - if (![userId isEqualToString:userMatrixId]) - { - Contact *splitContact = [[Contact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; - splitContact.mxMember = [self.mxRoom.state memberWithUserId:userId]; - [contacts addObject:splitContact]; - } + Contact *splitContact = [[Contact alloc] initMatrixContactWithDisplayName:contact.displayName andMatrixID:userId]; + splitContact.mxMember = [self.mxRoom.state memberWithUserId:userId]; + [contacts addObject:splitContact]; } } } else if (identifiers.count) { NSString *userId = identifiers.firstObject; - if (!mutableParticipants || [mutableParticipants indexOfObject:userId] == NSNotFound) + if ([contactsById objectForKey:userId] == nil) { - if (![userId isEqualToString:userMatrixId]) - { - [contacts addObject:contact]; - } + [contacts addObject:contact]; } } } + + // Copy participants and invited participants + participantsArray = [actualParticipants copy]; + invitedParticipantsArray = [invitedParticipants copy]; } - addParticipantsSearchText = searchText; + currentSearchText = searchText; - filteredParticipants = [NSMutableArray array]; - NSMutableArray *indexArray = [NSMutableArray array]; - NSInteger index = 0; - - // Show what the user is typing in a cell - // So that he can click on it + // Update invitable contacts list: + invitableContacts = [NSMutableArray array]; if (searchText.length) { + // Show what the user is typing in a cell. So that he can click on it MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:searchText andMatrixID:nil]; - [filteredParticipants addObject:contact]; - [indexArray addObject:[NSIndexPath indexPathForRow:index++ inSection:0]]; + [invitableContacts addObject:contact]; } - for (MXKContact* contact in contacts) { - if ([contact matchedWithPatterns:@[addParticipantsSearchText]]) + if ([contact matchedWithPatterns:@[currentSearchText]]) { - [filteredParticipants addObject:contact]; - [indexArray addObject:[NSIndexPath indexPathForRow:index++ inSection:0]]; + [invitableContacts addObject:contact]; } } - if ((searchResultSection != -1) && (previousFilteredCount || filteredParticipants.count)) + // Update filtered participants list + filteredActualParticipants = [NSMutableArray array]; + for (Contact *contact in participantsArray) { - NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(searchResultSection, 1)]; - [self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + if ([contact matchedWithPatterns:@[currentSearchText]]) + { + [filteredActualParticipants addObject:contact]; + } } + + // Update filtered invited participants list + filteredInvitedParticipants = [NSMutableArray array]; + for (Contact *contact in invitedParticipantsArray) + { + if ([contact matchedWithPatterns:@[currentSearchText]]) + { + [filteredInvitedParticipants addObject:contact]; + } + } + + // Refresh display + [self.tableView reloadData]; } - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar @@ -1216,8 +1408,6 @@ - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar [MXKAppSettings standardAppSettings].syncLocalContacts = YES; } - [self refreshSearchBarItemsColor:searchBar]; - return YES; } @@ -1236,8 +1426,10 @@ - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { - searchBar.text = addParticipantsSearchText = nil; - filteredParticipants = nil; + searchBar.text = currentSearchText = nil; + invitableContacts = nil; + filteredActualParticipants = nil; + filteredInvitedParticipants = nil; self.isAddParticipantSearchBarEditing = NO; // Leave search diff --git a/Vector/ViewController/RoomParticipantsViewController.xib b/Vector/ViewController/RoomParticipantsViewController.xib new file mode 100644 index 0000000000..d969c63477 --- /dev/null +++ b/Vector/ViewController/RoomParticipantsViewController.xib @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Vector/ViewController/RoomSettingsViewController.m b/Vector/ViewController/RoomSettingsViewController.m index 0931b2d685..67607542d5 100644 --- a/Vector/ViewController/RoomSettingsViewController.m +++ b/Vector/ViewController/RoomSettingsViewController.m @@ -43,7 +43,7 @@ #define ROOM_SECTION_MUTE_NOTIFICATIONS 4 #define ROOM_SECTION_COUNT 5 -#define ROOM_TOPIC_CELL_HEIGHT 99 +#define ROOM_TOPIC_CELL_HEIGHT 124 @interface RoomSettingsViewController () { @@ -630,9 +630,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N } roomTopicCell.mxkTextView.tintColor = kVectorColorGreen; + roomTopicCell.mxkTextView.font = [UIFont systemFontOfSize:16]; + roomTopicCell.mxkTextView.bounces = NO; roomTopicCell.mxkTextView.delegate = self; - // disable the edition if the user cannoy update it + // disable the edition if the user cannot update it roomTopicCell.mxkTextView.editable = mxRoom.isModerator; roomTopicCell.mxkTextView.textColor = kVectorTextColorGray; diff --git a/Vector/ViewController/RoomViewController.h b/Vector/ViewController/RoomViewController.h index 562562e17b..6bb39f61dc 100644 --- a/Vector/ViewController/RoomViewController.h +++ b/Vector/ViewController/RoomViewController.h @@ -18,20 +18,37 @@ #import "RoomTitleView.h" +#import "RoomPreviewData.h" + #import "UIViewController+VectorSearch.h" @interface RoomViewController : MXKRoomViewController // The expanded header @property (weak, nonatomic) IBOutlet UIView *expandedHeaderContainer; - @property (weak, nonatomic) IBOutlet NSLayoutConstraint *expandedHeaderContainerHeightConstraint; +// The preview header +@property (weak, nonatomic) IBOutlet UIScrollView *previewScrollView; +@property (weak, nonatomic) IBOutlet UIView *previewHeaderContainer; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; + /** - Hide/Show the expanded header. - By default this header is hidden on new instantiated RoomViewController object. + Force the display of the expanded header. + The default value is NO: this expanded header is hidden on new instantiated RoomViewController object. + + When this property is YES, the expanded header is forced each time the view controller appears. + */ +@property (nonatomic) BOOL showExpandedHeader; + +/** + 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)hideExpandedHeader:(BOOL)isHidden; +- (void)displayRoomPreview:(RoomPreviewData*)previewData; @end diff --git a/Vector/ViewController/RoomViewController.m b/Vector/ViewController/RoomViewController.m index b9302c4cdb..2f9abbea96 100644 --- a/Vector/ViewController/RoomViewController.m +++ b/Vector/ViewController/RoomViewController.m @@ -31,6 +31,7 @@ #import "RoomAvatarTitleView.h" #import "ExpandedRoomTitleView.h" #import "SimpleRoomTitleView.h" +#import "PreviewRoomTitleView.h" #import "RoomParticipantsViewController.h" @@ -62,13 +63,15 @@ #import "VectorDesignValues.h" +#import "GBDeviceInfo_iOS.h" + @interface RoomViewController () { // The expanded header ExpandedRoomTitleView *expandedHeader; - // The content offset at the beginning of scrolling - CGFloat storedContentOffset; + // The preview header + PreviewRoomTitleView *previewHeader; // The customized room data source for Vector RoomDataSource *customizedRoomDataSource; @@ -85,6 +88,12 @@ @interface RoomViewController () // The first tab is selected by default in room details screen in of case 'showRoomDetails' segue. // Use this flag to select a specific tab (0: people, 1: settings). NSUInteger selectedRoomDetailsIndex; + + // Preview data for a room invitation received by email or link to a room. + RoomPreviewData *roomPreviewData; + + // The position of the first touch down event stored in case of scrolling when the expanded header is visible. + CGPoint startScrollingPoint; } @property (strong, nonatomic) MXKAlert *currentAlert; @@ -109,6 +118,32 @@ + (instancetype)roomViewController #pragma mark - +- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) + { + // Disable auto join + self.autoJoinInvitedRoom = NO; + } + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + if (self) + { + // Disable auto join + self.autoJoinInvitedRoom = NO; + } + + return self; +} + +#pragma mark - + - (void)viewDidLoad { [super viewDidLoad]; @@ -135,21 +170,8 @@ - (void)viewDidLoad [self.bubblesTableView registerClass:RoomOutgoingTextMsgWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithoutSenderNameBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.class forCellReuseIdentifier:RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.defaultReuseIdentifier]; - // Set room title view - if (self.roomDataSource.isLive) - { - [self setRoomTitleViewClass:RoomTitleView.class]; - ((RoomTitleView*)self.titleView).tapGestureDelegate = self; - } - else - { - [self setRoomTitleViewClass:SimpleRoomTitleView.class]; - self.titleView.editable = NO; - } - // Prepare expanded header self.expandedHeaderContainer.backgroundColor = kVectorColorLightGrey; - self.expandedHeaderContainerHeightConstraint.constant = 237; expandedHeader = [ExpandedRoomTitleView roomTitleView]; expandedHeader.delegate = self; @@ -157,6 +179,13 @@ - (void)viewDidLoad expandedHeader.translatesAutoresizingMaskIntoConstraints = NO; [self.expandedHeaderContainer addSubview:expandedHeader]; // Force expanded header in full width + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:expandedHeader + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.expandedHeaderContainer + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:0]; NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:expandedHeader attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual @@ -171,31 +200,27 @@ - (void)viewDidLoad attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0]; - // Vertical constraints are required for iOS > 8 - NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:expandedHeader - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.expandedHeaderContainer - attribute:NSLayoutAttributeTop - multiplier:1.0 - constant:0]; - NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:expandedHeader - attribute:NSLayoutAttributeBottom - relatedBy:NSLayoutRelationEqual - toItem:self.expandedHeaderContainer - attribute:NSLayoutAttributeBottom - multiplier:1.0 - constant:0]; - [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]]; + [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint]]; + + + UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onSwipeGesture:)]; + [swipe setNumberOfTouchesRequired:1]; + [swipe setDirection:UISwipeGestureRecognizerDirectionUp]; + [self.expandedHeaderContainer addGestureRecognizer:swipe]; + + // Prepare preview header container + self.previewHeaderContainer.backgroundColor = kVectorColorLightGrey; // Replace the default input toolbar view. // Note: this operation will force the layout of subviews. That is why cell view classes must be registered before. [self setRoomInputToolbarViewClass:RoomInputToolbarView.class]; - // Disable animation during the update of the inputToolBar height. + // Update the inputToolBar height. + CGFloat height = (self.inputToolbarView ? ((RoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant : 0); + // Disable animation during the update [UIView setAnimationsEnabled:NO]; - [self roomInputToolbarView:self.inputToolbarView heightDidChanged:((RoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant completion:nil]; + [self roomInputToolbarView:self.inputToolbarView heightDidChanged:height completion:nil]; [UIView setAnimationsEnabled:YES]; // set extra area @@ -208,25 +233,14 @@ - (void)viewDidLoad self.navigationItem.rightBarButtonItem.target = self; self.navigationItem.rightBarButtonItem.action = @selector(onButtonPressed:); - // Handle potential data source + // Set up the room title view according to the data source (if any) + [self refreshRoomTitle]; + + // Refresh tool bar if the room data source is set. if (self.roomDataSource) { - if (self.roomDataSource.isLive) - { - self.navigationItem.rightBarButtonItem.enabled = YES; - } - else - { - // Hide the search button - self.navigationItem.rightBarButtonItem = nil; - } - [self refreshRoomInputToolbar]; } - else - { - self.navigationItem.rightBarButtonItem.enabled = NO; - } } - (void)didReceiveMemoryWarning @@ -239,7 +253,21 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; + // Refresh the room title view + [self refreshRoomTitle]; + + // Refresh tool bar if the room data source is set. + if (self.roomDataSource) + { + [self refreshRoomInputToolbar]; + } + [self listenTypingNotifications]; + + if (self.showExpandedHeader) + { + [self showExpandedHeader:YES]; + } } - (void)viewWillDisappear:(BOOL)animated @@ -264,8 +292,9 @@ - (void)viewWillDisappear:(BOOL)animated } } - // Hide expanded header to restore navigation bar settings - [self hideExpandedHeader:YES]; + // Hide expanded/preview header to restore navigation bar settings + [self showExpandedHeader:NO]; + [self showPreviewHeader:NO]; } - (void)viewDidAppear:(BOOL)animated @@ -316,12 +345,38 @@ - (void)viewDidLayoutSubviews UIEdgeInsets contentInset = self.bubblesTableView.contentInset; contentInset.bottom = self.bottomLayoutGuide.length; self.bubblesTableView.contentInset = contentInset; + + if (self.expandedHeaderContainer.isHidden == NO) + { + // Adjust the top constraint of the bubbles table + self.bubblesTableViewTopConstraint.constant = self.expandedHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top; + } } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator { - // Hide expanded header on device rotation - [self hideExpandedHeader:YES]; + // Hide the expanded header or the preview in case of iPad and iPhone 6 plus. + // On these devices, the display mode of the splitviewcontroller may change during screen rotation. + // It may correspond to an overlay mode in portrait and a side-by-side mode in landscape. + // This display mode change involves a change at the navigation bar level. + // If we don't hide the header, the navigation bar is in a wrong state after rotation. FIXME: Find a way to keep visible the header on rotation. + if ([GBDeviceInfo deviceInfo].display == GBDeviceDisplayiPad || [GBDeviceInfo deviceInfo].display >= GBDeviceDisplayiPhone55Inch) + { + // Hide expanded header on device rotation + [self showExpandedHeader:NO]; + + // Hide preview header (if any) during device rotation + BOOL isPreview = !self.previewScrollView.isHidden; + if (isPreview) + { + [self showPreviewHeader:NO]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((coordinator.transitionDuration + 0.5) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + + [self showPreviewHeader:YES]; + }); + } + } [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; } @@ -338,16 +393,9 @@ - (void)displayRoom:(MXKRoomDataSource *)dataSource { // This room view controller has its own typing management. self.roomDataSource.showTypingNotifications = NO; - - if (self.roomDataSource.isLive) - { - self.navigationItem.rightBarButtonItem.enabled = YES; - } - else - { - // Hide the search button - self.navigationItem.rightBarButtonItem = nil; - } + + // Set room title view + [self refreshRoomTitle]; // Store ref on customized room data source if ([dataSource isKindOfClass:RoomDataSource.class]) @@ -363,15 +411,101 @@ - (void)displayRoom:(MXKRoomDataSource *)dataSource [self refreshRoomInputToolbar]; } +- (void)onRoomDataSourceReady +{ + // Handle here invitation + if (self.roomDataSource.room.state.membership == MXMembershipInvite) + { + self.navigationItem.rightBarButtonItem.enabled = NO; + + // Show preview header + [self showPreviewHeader:YES]; + } + else + { + [super onRoomDataSourceReady]; + } +} + - (void)updateViewControllerAppearanceOnRoomDataSourceState { [super updateViewControllerAppearanceOnRoomDataSourceState]; - self.navigationItem.rightBarButtonItem.enabled = (self.roomDataSource != nil); + if (self.isRoomPreview) + { + self.navigationItem.rightBarButtonItem.enabled = NO; + + // Remove input tool bar and activity view if any + if (self.inputToolbarView) + { + [super setRoomInputToolbarViewClass:nil]; + } + if (self.activitiesView) + { + [super setRoomActivitiesViewClass:nil]; + } + + if (previewHeader) + { + previewHeader.mxRoom = self.roomDataSource.room; + self.previewHeaderContainerHeightConstraint.constant = previewHeader.bottomBorderView.frame.origin.y + 1; + } + } + else + { + [self showPreviewHeader:NO]; + + self.navigationItem.rightBarButtonItem.enabled = (self.roomDataSource != nil); + + self.titleView.editable = NO; + + // Force expanded header refresh + expandedHeader.mxRoom = self.roomDataSource.room; + self.expandedHeaderContainerHeightConstraint.constant = expandedHeader.bottomBorderView.frame.origin.y + 1; + + // Restore tool bar view and room activities view if none + if (!self.inputToolbarView) + { + [self setRoomInputToolbarViewClass:RoomInputToolbarView.class]; + + // Update the inputToolBar height. + CGFloat height = (self.inputToolbarView ? ((RoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant : 0); + // Disable animation during the update + [UIView setAnimationsEnabled:NO]; + [self roomInputToolbarView:self.inputToolbarView heightDidChanged:height completion:nil]; + [UIView setAnimationsEnabled:YES]; + + [self refreshRoomInputToolbar]; + } + + if (!self.activitiesView) + { + // And the extra area + [self setRoomActivitiesViewClass:RoomActivitiesView.class]; + } + } +} + +- (void)setRoomInputToolbarViewClass:(Class)roomInputToolbarViewClass +{ + // Do not show toolbar in case of preview + if (self.isRoomPreview) + { + roomInputToolbarViewClass = nil; + } - self.titleView.editable = NO; + [super setRoomInputToolbarViewClass:roomInputToolbarViewClass]; +} + +- (void)setRoomActivitiesViewClass:(Class)roomActivitiesViewClass +{ + // Do not show room activities in case of preview + if (self.isRoomPreview) + { + roomActivitiesViewClass = nil; + } - expandedHeader.mxRoom = self.roomDataSource.room; + [super setRoomActivitiesViewClass:roomActivitiesViewClass]; } - (BOOL)isIRCStyleCommand:(NSString*)string @@ -391,7 +525,7 @@ - (BOOL)isIRCStyleCommand:(NSString*)string [self.mainSession joinRoom:roomAlias success:^(MXRoom *room) { // Show the room - [[AppDelegate theDelegate] showRoom:room.state.roomId withMatrixSession:self.mainSession]; + [[AppDelegate theDelegate] showRoom:room.state.roomId andEventId:nil withMatrixSession:self.mainSession]; } failure:^(NSError *error) { NSLog(@"[Vector RoomVC] Join roomAlias (%@) failed", roomAlias); @@ -419,7 +553,7 @@ - (void)setKeyboardHeight:(CGFloat)keyboardHeight // Dispatch this operation to prevent flickering in navigation bar. dispatch_async(dispatch_get_main_queue(), ^{ - [self hideExpandedHeader:YES]; + [self showExpandedHeader:NO]; }); } @@ -441,11 +575,93 @@ - (void)destroy customizedRoomDataSource = nil; } + if (expandedHeader) + { + [expandedHeader removeFromSuperview]; + expandedHeader = nil; + } + + if (previewHeader) + { + [previewHeader removeFromSuperview]; + previewHeader = nil; + } + [super destroy]; } +#pragma mark - + +- (void)setShowExpandedHeader:(BOOL)showExpandedHeader +{ + _showExpandedHeader = showExpandedHeader; + [self showExpandedHeader:showExpandedHeader]; +} + #pragma mark - Internals +- (BOOL)isRoomPreview +{ + if (self.roomDataSource && self.roomDataSource.state == MXKDataSourceStateReady && self.roomDataSource.room.state.membership == MXMembershipInvite) + { + return YES; + } + + if (roomPreviewData) + { + return YES; + } + + return NO; +} + +- (void)refreshRoomTitle +{ + // Set the right room title view + if (self.isRoomPreview) + { + // Disable the search button + self.navigationItem.rightBarButtonItem.enabled = NO; + + [self showPreviewHeader:YES]; + } + else if (self.roomDataSource) + { + [self showPreviewHeader:NO]; + + if (self.roomDataSource.isLive) + { + // Enable the search button + self.navigationItem.rightBarButtonItem.enabled = YES; + + // Do not change title view class here if the expanded header is visible. + if (self.expandedHeaderContainer.hidden) + { + [self setRoomTitleViewClass:RoomTitleView.class]; + ((RoomTitleView*)self.titleView).tapGestureDelegate = self; + } + else + { + // Force expanded header refresh + expandedHeader.mxRoom = self.roomDataSource.room; + self.expandedHeaderContainerHeightConstraint.constant = expandedHeader.bottomBorderView.frame.origin.y + 1; + } + } + else + { + // Hide the search button + self.navigationItem.rightBarButtonItem = nil; + + [self setRoomTitleViewClass:SimpleRoomTitleView.class]; + self.titleView.editable = NO; + } + } + else + { + self.navigationItem.rightBarButtonItem.enabled = NO; + } +} + - (void)refreshRoomInputToolbar { // Check whether the input toolbar is ready before updating it. @@ -475,19 +691,36 @@ - (void)refreshRoomInputToolbar } } +- (void)onSwipeGesture:(UISwipeGestureRecognizer*)swipeGestureRecognizer +{ + UIView *view = swipeGestureRecognizer.view; + + if (view == self.expandedHeaderContainer) + { + // Hide the expanded header when user swipes upward on expanded header. + // We reset here the property 'showExpandedHeader'. Then the header is not expanded automatically on viewWillAppear. + self.showExpandedHeader = NO; + } +} + #pragma mark - Hide/Show expanded header -- (void)hideExpandedHeader:(BOOL)isHidden +- (void)showExpandedHeader:(BOOL)isVisible { - // Check conditions before applying change on room header - // This operation is ignored when a screen rotation is in progress, or when the room data source has been removed. - if (self.expandedHeaderContainer.isHidden != isHidden && isSizeTransitionInProgress == NO && self.roomDataSource) + // Check conditions before applying change on room header. + // This operation is ignored: + // - if a screen rotation is in progress. + // - if the room data source has been removed. + // - if the room data source does not manage a live timeline. + // - if the user's membership is not 'join'. + // - if the view controller is not embedded inside a split view controller yet. + if (self.expandedHeaderContainer.isHidden == isVisible && isSizeTransitionInProgress == NO && self.roomDataSource && self.roomDataSource.isLive && self.roomDataSource.room.state.membership == MXMembershipJoin && self.splitViewController) { - self.expandedHeaderContainer.hidden = isHidden; + self.expandedHeaderContainer.hidden = !isVisible; // Consider the main navigation controller if the current view controller is embedded inside a split view controller. UINavigationController *mainNavigationController = self.navigationController; - if (self.splitViewController && self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count) + if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count) { mainNavigationController = self.splitViewController.viewControllers.firstObject; } @@ -500,12 +733,135 @@ - (void)hideExpandedHeader:(BOOL)isHidden UIImage *shadowImage = nil; MXKImageView *roomAvatarView = nil; - if (isHidden) + if (isVisible) + { + [self setRoomTitleViewClass:RoomAvatarTitleView.class]; + // Note the avatar title view does not define tap gesture. + + roomAvatarView = ((RoomAvatarTitleView*)self.titleView).roomAvatar; + roomAvatarView.alpha = 0.0; + + shadowImage = [[UIImage alloc] init]; + + // Dismiss the keyboard when header is expanded. + [self.inputToolbarView dismissKeyboard]; + } + else { [self setRoomTitleViewClass:RoomTitleView.class]; ((RoomTitleView*)self.titleView).tapGestureDelegate = self; } + + // Report shadow image + [mainNavigationController.navigationBar setShadowImage:shadowImage]; + [mainNavigationController.navigationBar setBackgroundImage:shadowImage forBarMetrics:UIBarMetricsDefault]; + + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn + animations:^{ + self.bubblesTableViewTopConstraint.constant = (isVisible ? self.expandedHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top : 0); + + if (roomAvatarView) + { + roomAvatarView.alpha = 1; + } + + // Force to render the view + [self.view layoutIfNeeded]; + } + completion:^(BOOL finished){ + }]; + } +} + +#pragma mark - Hide/Show preview header + +- (void)showPreviewHeader:(BOOL)isVisible +{ + // This operation is ignored if a screen rotation is in progress, + // or if the view controller is not embedded inside a split view controller yet. + if (self.previewScrollView.isHidden == isVisible && isSizeTransitionInProgress == NO && self.splitViewController) + { + if (isVisible) + { + previewHeader = [PreviewRoomTitleView roomTitleView]; + previewHeader.delegate = self; + previewHeader.tapGestureDelegate = self; + previewHeader.translatesAutoresizingMaskIntoConstraints = NO; + [self.previewHeaderContainer addSubview:previewHeader]; + // Force preview header in full width + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:previewHeader + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.previewHeaderContainer + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:previewHeader + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.previewHeaderContainer + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0]; + // Vertical constraints are required for iOS > 8 + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:previewHeader + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.previewHeaderContainer + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:0]; + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:previewHeader + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.previewHeaderContainer + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0]; + + [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]]; + + if (self.roomDataSource) + { + previewHeader.mxRoom = self.roomDataSource.room; + } + else if (roomPreviewData) + { + previewHeader.roomPreviewData = roomPreviewData; + + if (roomPreviewData.emailInvitation.email) + { + // Warn the user that the email is not bound to his matrix account + previewHeader.subInvitationLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_preview_unlinked_email_warning", @"Vector", nil), roomPreviewData.emailInvitation.email]; + } + } + + self.previewHeaderContainerHeightConstraint.constant = previewHeader.bottomBorderView.frame.origin.y + 1; + } else + { + [previewHeader removeFromSuperview]; + previewHeader = nil; + } + + self.previewScrollView.hidden = !isVisible; + + // Consider the main navigation controller if the current view controller is embedded inside a split view controller. + UINavigationController *mainNavigationController = self.navigationController; + if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count) + { + mainNavigationController = self.splitViewController.viewControllers.firstObject; + } + + // When the expanded header is displayed, we hide the bottom border of the navigation bar (the shadow image). + // The default shadow image is nil. When non-nil, this property represents a custom shadow image to show instead + // of the default. For a custom shadow image to be shown, a custom background image must also be set with the + // setBackgroundImage:forBarMetrics: method. If the default background image is used, then the default shadow + // image will be used regardless of the value of this property. + UIImage *shadowImage = nil; + MXKImageView *roomAvatarView = nil; + + if (isVisible) { [self setRoomTitleViewClass:RoomAvatarTitleView.class]; // Note the avatar title view does not define tap gesture. @@ -514,9 +870,21 @@ - (void)hideExpandedHeader:(BOOL)isHidden roomAvatarView.alpha = 0.0; shadowImage = [[UIImage alloc] init]; - - // Dismiss the keyboard when header is expanded. - [self.inputToolbarView dismissKeyboard]; + + // Set the avatar provided in preview data + if (roomPreviewData.roomAvatarUrl) + { + RoomAvatarTitleView *roomAvatarTitleView = (RoomAvatarTitleView*)self.titleView; + MXKImageView *roomAvatarView = roomAvatarTitleView.roomAvatar; + NSString *roomAvatarUrl = [self.mainSession.matrixRestClient urlOfContentThumbnail:roomPreviewData.roomAvatarUrl toFitViewSize:roomAvatarView.frame.size withMethod:MXThumbnailingMethodCrop]; + + roomAvatarTitleView.roomAvatarURL = roomAvatarUrl; + } + } + else + { + [self setRoomTitleViewClass:RoomTitleView.class]; + // We don't want to handle tap gesture here } // Report shadow image @@ -525,7 +893,6 @@ - (void)hideExpandedHeader:(BOOL)isHidden [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ - self.bubblesTableViewTopConstraint.constant = (isHidden ? 0 : self.expandedHeaderContainerHeightConstraint.constant - self.bubblesTableView.contentInset.top); if (roomAvatarView) { @@ -540,6 +907,20 @@ - (void)hideExpandedHeader:(BOOL)isHidden } } +#pragma mark - Preview + +- (void)displayRoomPreview:(RoomPreviewData *)previewData +{ + if (previewData) + { + [self addMatrixSession:previewData.mxSession]; + + roomPreviewData = previewData; + + [self refreshRoomTitle]; + } +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -962,6 +1343,33 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac } } +- (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue +{ + BOOL shouldDoAction = defaultValue; + + if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellShouldInteractWithURL]) + { + // Try to catch universal link supported by the app + NSURL *url = userInfo[kMXKRoomBubbleCellUrl]; + + // iOS Patch: fix vector.im urls before using it + if ([url.host isEqualToString:@"vector.im"]) + { + url = [AppDelegate fixURLWithSeveralHashKeys:url]; + + // If the link can be open it by the app, let it do + if ([[AppDelegate theDelegate] isUniversalLink:url]) + { + shouldDoAction = NO; + + [[AppDelegate theDelegate] handleUniversalLinkFragment:url.fragment]; + } + } + } + + return shouldDoAction; +} + - (void)cancelEventSelection { if (self.currentAlert) @@ -1099,27 +1507,75 @@ - (IBAction)onButtonPressed:(id)sender } } -#pragma mark - UITableView delegate +#pragma mark - UITableViewDelegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [super tableView:tableView didSelectRowAtIndexPath:indexPath]; } +#pragma mark - + - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { - // Store the current offset to detect scroll down - storedContentOffset = scrollView.contentOffset.y; + if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewWillBeginDragging:)]) + { + [super scrollViewWillBeginDragging:scrollView]; + } + + if (self.expandedHeaderContainer.isHidden == NO) + { + // Store here the position of the first touch down event + UIPanGestureRecognizer *panGestureRecognizer = scrollView.panGestureRecognizer; + if (panGestureRecognizer && panGestureRecognizer.numberOfTouches) + { + startScrollingPoint = [panGestureRecognizer locationOfTouch:0 inView:self.view]; + } + else + { + startScrollingPoint = CGPointZero; + } + } } -- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { - [super scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) + { + [super scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; + } - // Hide expanded header on scroll down - if (storedContentOffset < scrollView.contentOffset.y) + if (decelerate == NO) + { + // Handle swipe on expanded header + [self onScrollViewDidEndScrolling:scrollView]; + } + else + { + // Dispatch async the expanded header handling in order to let the deceleration go first. + dispatch_async(dispatch_get_main_queue(), ^{ + + // Handle swipe on expanded header + [self onScrollViewDidEndScrolling:scrollView]; + + }); + } +} + +- (void)onScrollViewDidEndScrolling:(UIScrollView *)scrollView +{ + // Check whether the user's finger has been dragged over the expanded header. + // In that case the expanded header is collapsed + if (self.expandedHeaderContainer.isHidden == NO && (startScrollingPoint.y != 0)) { - [self hideExpandedHeader:YES]; + UIPanGestureRecognizer *panGestureRecognizer = scrollView.panGestureRecognizer; + CGPoint translate = [panGestureRecognizer translationInView:self.view]; + + if (startScrollingPoint.y + translate.y < self.expandedHeaderContainer.frame.size.height) + { + // Hide the expanded header by reseting the property 'showExpandedHeader'. Then the header is not expanded automatically on viewWillAppear. + self.showExpandedHeader = NO; + } } } @@ -1142,7 +1598,7 @@ - (void)roomTitleView:(RoomTitleView*)titleView recognizeTapGesture:(UITapGestur if (self.expandedHeaderContainer.isHidden) { // Expand the header - [self hideExpandedHeader:NO]; + [self showExpandedHeader:YES]; } else { @@ -1157,6 +1613,76 @@ - (void)roomTitleView:(RoomTitleView*)titleView recognizeTapGesture:(UITapGestur selectedRoomDetailsIndex = 0; [self performSegueWithIdentifier:@"showRoomDetails" sender:self]; } + else if (view == previewHeader.leftButton) + { + if (roomPreviewData) + { + // Attempt to join the room + // Note in case of simple link to a room the signUrl param is nil + [self joinRoomWithRoomId:roomPreviewData.roomId andSignUrl:roomPreviewData.emailInvitation.signUrl completion:^(BOOL succeed) { + + if (succeed) + { + roomPreviewData = nil; + + // Enable back the text input + [self setRoomInputToolbarViewClass:RoomInputToolbarView.class]; + + // Update the inputToolBar height. + CGFloat height = (self.inputToolbarView ? ((RoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant : 0); + // Disable animation during the update + [UIView setAnimationsEnabled:NO]; + [self roomInputToolbarView:self.inputToolbarView heightDidChanged:height completion:nil]; + [UIView setAnimationsEnabled:YES]; + + // And the extra area + [self setRoomActivitiesViewClass:RoomActivitiesView.class]; + + [self refreshRoomTitle]; + [self refreshRoomInputToolbar]; + } + + }]; + } + else + { + [self joinRoom:^(BOOL succeed) { + + if (succeed) + { + [self refreshRoomTitle]; + } + + }]; + } + } + else if (view == previewHeader.rightButton) + { + if (roomPreviewData) + { + // Decline this invitation = leave this page + [[AppDelegate theDelegate] restoreInitialDisplay:^{}]; + } + else + { + [self startActivityIndicator]; + + [self.roomDataSource.room leave:^{ + + [self stopActivityIndicator]; + + // We remove the current view controller. + // Pop to homes view controller + [[AppDelegate theDelegate] restoreInitialDisplay:^{}]; + + } failure:^(NSError *error) { + + [self stopActivityIndicator]; + NSLog(@"[Vector RoomVC] Failed to reject an invited room (%@) failed", self.roomDataSource.room.state.roomId); + + }]; + } + } } #pragma mark - Typing management diff --git a/Vector/ViewController/RoomViewController.xib b/Vector/ViewController/RoomViewController.xib index cbd68153f1..9536981058 100644 --- a/Vector/ViewController/RoomViewController.xib +++ b/Vector/ViewController/RoomViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -12,6 +12,9 @@ + + + @@ -30,12 +33,31 @@ + @@ -55,17 +77,21 @@ - + + + - + - + + + diff --git a/Vector/ViewController/SegmentedViewController.h b/Vector/ViewController/SegmentedViewController.h index b23bdd468e..422acdcb80 100644 --- a/Vector/ViewController/SegmentedViewController.h +++ b/Vector/ViewController/SegmentedViewController.h @@ -41,6 +41,11 @@ limitations under the License. */ @property (nonatomic, readonly) UIViewController *selectedViewController; +/** + The view controllers managed by this SegmentedViewController instance. + */ +@property (nonatomic, readonly) NSArray *viewControllers; + /** Returns the `UINib` object initialized for a `SegmentedViewController`. diff --git a/Vector/ViewController/SegmentedViewController.m b/Vector/ViewController/SegmentedViewController.m index 6b27f1851f..acf356cb18 100644 --- a/Vector/ViewController/SegmentedViewController.m +++ b/Vector/ViewController/SegmentedViewController.m @@ -82,6 +82,11 @@ - (void)setSelectedIndex:(NSUInteger)selectedIndex } } +- (NSArray *)viewControllers +{ + return viewControllers; +} + #pragma mark - - (void)addConstraint:(UIView*)view constraint:(NSLayoutConstraint*)aConstraint diff --git a/Vector/ViewController/SettingsViewController.m b/Vector/ViewController/SettingsViewController.m index d3127d6aab..bfa3ce6168 100644 --- a/Vector/ViewController/SettingsViewController.m +++ b/Vector/ViewController/SettingsViewController.m @@ -1131,7 +1131,7 @@ - (IBAction)onAddNewEmail:(id)sender MXSession* session = [[AppDelegate theDelegate].mxSessions objectAtIndex:0]; MXK3PID *new3PID = [[MXK3PID alloc] initWithMedium:kMX3PIDMediumEmail andAddress:newEmailTextField.text]; - [new3PID requestValidationTokenWithMatrixRestClient:session.matrixRestClient success:^{ + [new3PID requestValidationTokenWithMatrixRestClient:session.matrixRestClient nextLink:nil success:^{ [self showValidationEmailDialogWithMessage:[NSBundle mxk_localizedStringForKey:@"account_email_validation_message"] for3PID:new3PID]; diff --git a/Vector/Views/Authentication/AuthInputsView.m b/Vector/Views/Authentication/AuthInputsView.m index 48bb28a4d2..8d02a16113 100644 --- a/Vector/Views/Authentication/AuthInputsView.m +++ b/Vector/Views/Authentication/AuthInputsView.m @@ -20,7 +20,15 @@ @interface AuthInputsView () { + /** + The current email validation + */ MXK3PID *submittedEmail; + + /** + The set of parameters ready to use for a registration. + */ + NSDictionary *externalRegistrationParameters; } @end @@ -65,6 +73,16 @@ - (BOOL)setAuthSession:(MXAuthenticationSession *)authSession withAuthType:(MXKA // Validate first the provided session MXAuthenticationSession *validSession = [self validateAuthenticationSession:authSession]; + // Cancel email validation if any + if (submittedEmail) + { + [submittedEmail cancelCurrentRequest]; + submittedEmail = nil; + } + + // Reset external registration parameters + externalRegistrationParameters = nil; + // Reset UI by hidding all items [self hideInputsContainer]; @@ -130,6 +148,12 @@ - (BOOL)setAuthSession:(MXAuthenticationSession *)authSession withAuthType:(MXKA - (NSString*)validateParameters { + // Consider everything is fine when external registration parameters are ready to use + if (externalRegistrationParameters) + { + return nil; + } + // Check the validity of the parameters NSString *errorMsg = nil; @@ -210,6 +234,16 @@ - (void)prepareParameters:(void (^)(NSDictionary *parameters))callback { if (callback) { + // Return external registration parameters if any + if (externalRegistrationParameters) + { + // We trigger here a registration based on external inputs. All the required data are handled by the session id. + NSLog(@"[AuthInputsView] prepareParameters: return external registration parameters"); + callback(externalRegistrationParameters); + + // CAUTION: Do not reset this dictionary here, it is used later to handle this registration until the end (see [updateAuthSessionWithCompletedStages:didUpdateParameters:]) + } + // Prepare here parameters dict by checking each required fields. NSDictionary *parameters = nil; @@ -275,7 +309,20 @@ - (void)prepareParameters:(void (^)(NSDictionary *parameters))callback { // Launch email validation submittedEmail = [[MXK3PID alloc] initWithMedium:kMX3PIDMediumEmail andAddress:self.emailTextField.text]; + + // Create the next link that is common to all Vector.im clients + // FIXME: When available, use the prod Vector web app URL + NSString *webAppUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"webAppUrlDev"]; + + NSString *nextLink = [NSString stringWithFormat:@"%@/#/register?client_secret=%@&hs_url=%@&is_url=%@&session_id=%@", + webAppUrl, + [submittedEmail.clientSecret stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]], + [restClient.homeserver stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]], + [restClient.identityServer stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]], + [currentSession.session stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]; + [submittedEmail requestValidationTokenWithMatrixRestClient:restClient + nextLink:nextLink success:^{ NSURL *identServerURL = [NSURL URLWithString:restClient.identityServer]; @@ -298,6 +345,12 @@ - (void)prepareParameters:(void (^)(NSDictionary *parameters))callback NSLog(@"[AuthInputsView] Failed to request email token: %@", error); + // Ignore connection cancellation error + if (([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled)) + { + return; + } + callback(nil); }]; @@ -367,12 +420,25 @@ - (void)updateAuthSessionWithCompletedStages:(NSArray *)completedStages didUpdat if (response.length) { // Update the parameters dict - NSDictionary *parameters = @{ - @"auth": @{@"session": currentSession.session, @"response": response, @"type": kMXLoginFlowTypeRecaptcha}, - @"username": self.userLoginTextField.text, - @"password": self.passWordTextField.text, - @"bind_email": @(YES) - }; + NSDictionary *parameters; + + if (externalRegistrationParameters) + { + // We finalize here a registration triggered from external inputs. All the required data are handled by the session id + parameters = @{ + @"auth": @{@"session": currentSession.session, @"response": response, @"type": kMXLoginFlowTypeRecaptcha}, + }; + } + else + { + parameters = @{ + @"auth": @{@"session": currentSession.session, @"response": response, @"type": kMXLoginFlowTypeRecaptcha}, + @"username": self.userLoginTextField.text, + @"password": self.passWordTextField.text, + @"bind_email": @(YES) + }; + } + callback (parameters); } @@ -393,6 +459,100 @@ - (void)updateAuthSessionWithCompletedStages:(NSArray *)completedStages didUpdat } } +- (BOOL)setExternalRegistrationParameters:(NSDictionary *)registrationParameters +{ + // Presently we only support a registration based on next_link associated to a successful email validation. + NSString *homeserverURL; + NSString *identityURL; + + // Check the current authentication type + if (self.authType != MXKAuthenticationTypeRegister) + { + NSLog(@"[AuthInputsView] setExternalRegistrationParameters failed: wrong auth type"); + return NO; + } + + // Retrieve the REST client from delegate + MXRestClient *restClient; + if (self.delegate && [self.delegate respondsToSelector:@selector(authInputsViewEmailValidationRestClient:)]) + { + restClient = [self.delegate authInputsViewEmailValidationRestClient:self]; + } + + if (restClient) + { + // Sanity check on home server + id hs_url = registrationParameters[@"hs_url"]; + if (hs_url && [hs_url isKindOfClass:NSString.class]) + { + homeserverURL = hs_url; + + if ([homeserverURL isEqualToString:restClient.homeserver] == NO) + { + NSLog(@"[AuthInputsView] setExternalRegistrationParameters failed: wrong homeserver URL"); + return NO; + } + } + + // Sanity check on identity server + id is_url = registrationParameters[@"is_url"]; + if (is_url && [is_url isKindOfClass:NSString.class]) + { + identityURL = is_url; + + if ([identityURL isEqualToString:restClient.identityServer] == NO) + { + NSLog(@"[AuthInputsView] setExternalRegistrationParameters failed: wrong identity server URL"); + return NO; + } + } + } + else + { + NSLog(@"[AuthInputsView] setExternalRegistrationParameters failed: not supported"); + return NO; + } + + // Retrieve other parameters + NSString *clientSecret; + NSString *sid; + NSString *sessionId; + + id value = registrationParameters[@"client_secret"]; + if (value && [value isKindOfClass:NSString.class]) + { + clientSecret = value; + } + value = registrationParameters[@"sid"]; + if (value && [value isKindOfClass:NSString.class]) + { + sid = value; + } + value = registrationParameters[@"session_id"]; + if (value && [value isKindOfClass:NSString.class]) + { + sessionId = value; + } + + // Check validity of the required parameters + if (!homeserverURL.length || !identityURL.length || !clientSecret.length || !sid.length || !sessionId.length) + { + NSLog(@"[AuthInputsView] setExternalRegistrationParameters failed: wrong parameters"); + return NO; + } + + // Prepare the registration parameters (Ready to use) + NSURL *identServerURL = [NSURL URLWithString:identityURL]; + externalRegistrationParameters = @{ + @"auth": @{@"session": sessionId, @"threepid_creds": @{@"client_secret": clientSecret, @"id_server": identServerURL.host, @"sid": sid}, @"type": kMXLoginFlowTypeEmailIdentity}, + }; + + // Hide all inputs by default + [self hideInputsContainer]; + + return YES; +} + - (BOOL)areAllRequiredFieldsSet { // BOOL ret = [super areAllRequiredFieldsSet]; @@ -431,6 +591,11 @@ - (NSString*)userId return self.userLoginTextField.text; } +- (NSString*)password +{ + return self.passWordTextField.text; +} + #pragma mark - UITextField delegate - (BOOL)textFieldShouldReturn:(UITextField*)textField diff --git a/Vector/Views/Contact/ContactTableViewCell.h b/Vector/Views/Contact/ContactTableViewCell.h index d1f1594c88..7f52810980 100644 --- a/Vector/Views/Contact/ContactTableViewCell.h +++ b/Vector/Views/Contact/ContactTableViewCell.h @@ -25,12 +25,11 @@ */ @interface ContactTableViewCell : MXKTableViewCell -@property (strong, nonatomic) IBOutlet MXKImageView *thumbnailView; -@property (strong, nonatomic) IBOutlet UILabel *contactDisplayNameLabel; -@property (weak, nonatomic) IBOutlet UILabel *lastPresenceLabel; -@property (weak, nonatomic) IBOutlet UIView *bottomLineSeparator; -@property (weak, nonatomic) IBOutlet UIView *topLineSeparator; -@property (weak, nonatomic) IBOutlet UIView *customAccessoryView; +@property (nonatomic) IBOutlet MXKImageView *thumbnailView; +@property (nonatomic) IBOutlet UIImageView *thumbnailBadgeView; +@property (nonatomic) IBOutlet UILabel *contactDisplayNameLabel; +@property (nonatomic) IBOutlet UILabel *lastPresenceLabel; +@property (nonatomic) IBOutlet UIView *customAccessoryView; @property (nonatomic) BOOL showCustomAccessoryView; diff --git a/Vector/Views/Contact/ContactTableViewCell.m b/Vector/Views/Contact/ContactTableViewCell.m index 5bee696282..845aee9aba 100644 --- a/Vector/Views/Contact/ContactTableViewCell.m +++ b/Vector/Views/Contact/ContactTableViewCell.m @@ -47,8 +47,6 @@ - (void)awakeFromNib self.thumbnailView.clipsToBounds = YES; // apply the vector colours - self.bottomLineSeparator.backgroundColor = kVectorColorSiver; - self.topLineSeparator.backgroundColor = kVectorColorSiver; self.lastPresenceLabel.textColor = kVectorTextColorGray; } @@ -198,29 +196,8 @@ - (void)refreshContactPresence { NSString* presenceText = nil; NSString* matrixId = [self getFirstMatrixId]; - MXRoomMember* member = nil; - if (self.mxRoom && matrixId) - { - member = [self.mxRoom.state memberWithUserId:matrixId]; - } - - if (member && (member.membership != MXMembershipJoin)) - { - if (member.membership == MXMembershipInvite) - { - presenceText = NSLocalizedStringFromTable(@"room_participants_invite", @"Vector", nil); - } - else if (member.membership == MXMembershipLeave) - { - presenceText = NSLocalizedStringFromTable(@"room_participants_leave", @"Vector", nil); - } - else if (member.membership == MXMembershipBan) - { - presenceText = NSLocalizedStringFromTable(@"room_participants_ban", @"Vector", nil); - } - } - else if (matrixId) + if (matrixId) { MXUser *user = nil; @@ -268,7 +245,7 @@ - (void)refreshContactPresence } else if (contact.isThirdPartyInvite) { - presenceText = NSLocalizedStringFromTable(@"room_participants_invite", @"Vector", nil); + presenceText = NSLocalizedStringFromTable(@"room_participants_offline", @"Vector", nil); } self.lastPresenceLabel.text = presenceText; diff --git a/Vector/Views/Contact/ContactTableViewCell.xib b/Vector/Views/Contact/ContactTableViewCell.xib index 24d28534b0..01e8229e9c 100644 --- a/Vector/Views/Contact/ContactTableViewCell.xib +++ b/Vector/Views/Contact/ContactTableViewCell.xib @@ -1,8 +1,8 @@ - + - + @@ -14,20 +14,6 @@ - - - - - - - - - - - - - - @@ -36,6 +22,13 @@ +