diff --git a/CHANGES.rst b/CHANGES.rst index 4e072709e..ae4a5aa13 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ Changes in 0.8.6 (2019-xx-xx) Improvements: * RoomVC: When replying, use a "Reply" button instead of "Send". + * RoomVC: New message actions (#2394). Changes in 0.8.5 (2019-xx-xx) =============================================== diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 08f2bccf2..0a23f01f5 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -189,6 +189,7 @@ B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1798301211B13B3001FD722 /* OnBoardingManager.swift */; }; B19EFA3921F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19EFA3821F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift */; }; B19EFA3B21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19EFA3A21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift */; }; + B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A5B33D227ADF2A004CBA85 /* UIImage.swift */; }; B1B5571820EE6C4D00210D55 /* CountryPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567A20EE6C4C00210D55 /* CountryPickerViewController.m */; }; B1B5571920EE6C4D00210D55 /* LanguagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567C20EE6C4C00210D55 /* LanguagePickerViewController.m */; }; B1B5571A20EE6C4D00210D55 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567E20EE6C4C00210D55 /* SettingsViewController.m */; }; @@ -426,6 +427,17 @@ B1B5599320EFC5E400210D55 /* DecryptionFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5598D20EFC5E400210D55 /* DecryptionFailure.m */; }; B1B5599420EFC5E400210D55 /* DecryptionFailureTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5599120EFC5E400210D55 /* DecryptionFailureTracker.m */; }; B1B9194C2118984300FE25B5 /* RoomPredecessorBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1B9194A2118984300FE25B5 /* RoomPredecessorBubbleCell.xib */; }; + B1C562CA2289C2690037F12A /* UIGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */; }; + B1C562CC228AB3510037F12A /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562CB228AB3510037F12A /* UIStackView.swift */; }; + B1C562D9228C0B760037F12A /* RoomContextualMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */; }; + B1C562DB228C0BB00037F12A /* RoomContextualMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DA228C0BB00037F12A /* RoomContextualMenuAction.swift */; }; + B1C562E1228C7C8C0037F12A /* RoomContextualMenuToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DC228C7C890037F12A /* RoomContextualMenuToolbarView.swift */; }; + B1C562E2228C7C8D0037F12A /* RoomContextualMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DD228C7C8A0037F12A /* RoomContextualMenuViewController.swift */; }; + B1C562E3228C7C8D0037F12A /* RoomContextualMenuPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DE228C7C8B0037F12A /* RoomContextualMenuPresenter.swift */; }; + B1C562E4228C7C8D0037F12A /* RoomContextualMenuToolbarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1C562DF228C7C8C0037F12A /* RoomContextualMenuToolbarView.xib */; }; + B1C562E5228C7C8D0037F12A /* RoomContextualMenuViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */; }; + B1C562E8228C7CF20037F12A /* ContextualMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */; }; + B1C562E9228C7CF20037F12A /* ContextualMenuItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */; }; B1CA3A2721EF6914000D1D89 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CA3A2621EF6913000D1D89 /* UIViewController.swift */; }; B1CA3A2921EF692B000D1D89 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CA3A2821EF692B000D1D89 /* UIView.swift */; }; B1CE9EFD22148703000FAE6A /* SignOutAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CE9EFC22148703000FAE6A /* SignOutAlertPresenter.swift */; }; @@ -760,6 +772,7 @@ B1798301211B13B3001FD722 /* OnBoardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingManager.swift; sourceTree = ""; }; B19EFA3821F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverCoordinatorType.swift; sourceTree = ""; }; B19EFA3A21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverCoordinator.swift; sourceTree = ""; }; + B1A5B33D227ADF2A004CBA85 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; B1B5567920EE6C4C00210D55 /* CountryPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountryPickerViewController.h; sourceTree = ""; }; B1B5567A20EE6C4C00210D55 /* CountryPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountryPickerViewController.m; sourceTree = ""; }; B1B5567C20EE6C4C00210D55 /* LanguagePickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LanguagePickerViewController.m; sourceTree = ""; }; @@ -1133,6 +1146,17 @@ B1B5599020EFC5E400210D55 /* Analytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Analytics.h; sourceTree = ""; }; B1B5599120EFC5E400210D55 /* DecryptionFailureTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DecryptionFailureTracker.m; sourceTree = ""; }; B1B9194A2118984300FE25B5 /* RoomPredecessorBubbleCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomPredecessorBubbleCell.xib; sourceTree = ""; }; + B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIGestureRecognizer.swift; sourceTree = ""; }; + B1C562CB228AB3510037F12A /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = ""; }; + B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuItem.swift; sourceTree = ""; }; + B1C562DA228C0BB00037F12A /* RoomContextualMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuAction.swift; sourceTree = ""; }; + B1C562DC228C7C890037F12A /* RoomContextualMenuToolbarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuToolbarView.swift; sourceTree = ""; }; + B1C562DD228C7C8A0037F12A /* RoomContextualMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuViewController.swift; sourceTree = ""; }; + B1C562DE228C7C8B0037F12A /* RoomContextualMenuPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuPresenter.swift; sourceTree = ""; }; + B1C562DF228C7C8C0037F12A /* RoomContextualMenuToolbarView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomContextualMenuToolbarView.xib; sourceTree = ""; }; + B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RoomContextualMenuViewController.storyboard; sourceTree = ""; }; + B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextualMenuItemView.swift; sourceTree = ""; }; + B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContextualMenuItemView.xib; sourceTree = ""; }; B1CA3A2621EF6913000D1D89 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; B1CA3A2821EF692B000D1D89 /* UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; B1CE9EFC22148703000FAE6A /* SignOutAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutAlertPresenter.swift; sourceTree = ""; }; @@ -1997,6 +2021,7 @@ B1B556A120EE6C4C00210D55 /* Files */, B1B556A420EE6C4C00210D55 /* Members */, B1B5569020EE6C4C00210D55 /* Settings */, + B1C562D7228C0B4C0037F12A /* ContextualMenu */, ); path = Room; sourceTree = ""; @@ -2977,6 +3002,22 @@ path = Analytics; sourceTree = ""; }; + B1C562D7228C0B4C0037F12A /* ContextualMenu */ = { + isa = PBXGroup; + children = ( + B1C562DA228C0BB00037F12A /* RoomContextualMenuAction.swift */, + B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */, + B1C562DE228C7C8B0037F12A /* RoomContextualMenuPresenter.swift */, + B1C562DD228C7C8A0037F12A /* RoomContextualMenuViewController.swift */, + B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */, + B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */, + B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */, + B1C562DC228C7C890037F12A /* RoomContextualMenuToolbarView.swift */, + B1C562DF228C7C8C0037F12A /* RoomContextualMenuToolbarView.xib */, + ); + path = ContextualMenu; + sourceTree = ""; + }; B1CE9EFB22148681000FAE6A /* SignOut */ = { isa = PBXGroup; children = ( @@ -3131,6 +3172,9 @@ B109D6F0222D8C400061B6D9 /* UIApplication.swift */, B1DB4F05223015080065DBFA /* Character.swift */, B1DB4F0A223131600065DBFA /* String.swift */, + B1A5B33D227ADF2A004CBA85 /* UIImage.swift */, + B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */, + B1C562CB228AB3510037F12A /* UIStackView.swift */, ); path = Categories; sourceTree = ""; @@ -3426,6 +3470,7 @@ B1B558EA20EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.xib in Resources */, B1B558CD20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */, B1B9194C2118984300FE25B5 /* RoomPredecessorBubbleCell.xib in Resources */, + B1C562E9228C7CF20037F12A /* ContextualMenuItemView.xib in Resources */, B1B5572120EE6C4D00210D55 /* ContactsTableViewController.xib in Resources */, B1B5593A20EF7BAC00210D55 /* TableViewCellWithLabelAndLargeTextView.xib in Resources */, B1B558D820EF768F00210D55 /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */, @@ -3496,6 +3541,7 @@ B1B557D720EF5EA900210D55 /* RoomActivitiesView.xib in Resources */, B1098BF821ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.storyboard in Resources */, F083BDF31E7009ED00A9B29C /* Images.xcassets in Resources */, + B1C562E4228C7C8D0037F12A /* RoomContextualMenuToolbarView.xib in Resources */, B1B5590720EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, B169329920F39E6300746532 /* LaunchScreen.storyboard in Resources */, B1B5595320EF9A8700210D55 /* RecentTableViewCell.xib in Resources */, @@ -3520,6 +3566,7 @@ B1B558C020EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, B1B5572420EE6C4D00210D55 /* RoomViewController.xib in Resources */, B169331520F3CAFC00746532 /* PublicRoomTableViewCell.xib in Resources */, + B1C562E5228C7C8D0037F12A /* RoomContextualMenuViewController.storyboard in Resources */, 3232ABA2225730E100AD6A5C /* DeviceVerificationStartViewController.storyboard in Resources */, 3284A35120A07C210044F922 /* postMessageAPI.js in Resources */, B1B557A220EF58AD00210D55 /* ContactTableViewCell.xib in Resources */, @@ -3808,6 +3855,7 @@ B1B5572F20EE6C4D00210D55 /* ReadReceiptsViewController.m in Sources */, B1B558CB20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, B169330B20F3CA3A00746532 /* Contact.m in Sources */, + B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */, B1D4752A21EE52B10067973F /* KeyBackupSetupIntroViewController.swift in Sources */, B1B5599220EFC5E400210D55 /* Analytics.m in Sources */, B14F143422144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewAction.swift in Sources */, @@ -3852,6 +3900,7 @@ 32891D712264DF7B00C82226 /* DeviceVerificationVerifiedViewController.swift in Sources */, F083BDEF1E7009ED00A9B29C /* UINavigationController+Riot.m in Sources */, B1B5581F20EF625800210D55 /* SimpleRoomTitleView.m in Sources */, + B1C562E2228C7C8D0037F12A /* RoomContextualMenuViewController.swift in Sources */, B169330020F3C97D00746532 /* RoomDataSource.m in Sources */, B1B558ED20EF768F00210D55 /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.m in Sources */, B1B5571920EE6C4D00210D55 /* LanguagePickerViewController.m in Sources */, @@ -3891,6 +3940,7 @@ 3232ABBA2257BE6500AD6A5C /* DeviceVerificationVerifyViewModel.swift in Sources */, B1098C1021ED07E4000DDA48 /* Presentable.swift in Sources */, B1B558E020EF768F00210D55 /* RoomOutgoingTextMsgBubbleCell.m in Sources */, + B1C562E3228C7C8D0037F12A /* RoomContextualMenuPresenter.swift in Sources */, B1B5593C20EF7BAC00210D55 /* TableViewCellWithCheckBoxes.m in Sources */, 32891D6B2264CBA300C82226 /* SimpleScreenTemplateViewController.swift in Sources */, B1CA3A2721EF6914000D1D89 /* UIViewController.swift in Sources */, @@ -3936,6 +3986,8 @@ B1B5572020EE6C4D00210D55 /* ContactsTableViewController.m in Sources */, B1B5581920EF625800210D55 /* RoomTitleView.m in Sources */, B1098BE321ECE09F000DDA48 /* RiotDefaults.swift in Sources */, + B1C562CA2289C2690037F12A /* UIGestureRecognizer.swift in Sources */, + B1C562CC228AB3510037F12A /* UIStackView.swift in Sources */, B1B557BE20EF5B4500210D55 /* RoomInputToolbarView.m in Sources */, B1B5573B20EE6C4D00210D55 /* FavouritesViewController.m in Sources */, B1B5579920EF575B00210D55 /* AuthInputsView.m in Sources */, @@ -3977,11 +4029,14 @@ 324A2054225FC571004FE8B0 /* DeviceVerificationIncomingCoordinatorType.swift in Sources */, 3232ABB92257BE6500AD6A5C /* DeviceVerificationVerifyViewController.swift in Sources */, B139C21F21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift in Sources */, + B1C562DB228C0BB00037F12A /* RoomContextualMenuAction.swift in Sources */, B1B5574720EE6C4D00210D55 /* UsersDevicesViewController.m in Sources */, B1098BFF21ECFE65000DDA48 /* PasswordStrengthView.swift in Sources */, B1B558D220EF768F00210D55 /* RoomEncryptedDataBubbleCell.m in Sources */, B1B558FA20EF768F00210D55 /* RoomMembershipBubbleCell.m in Sources */, 3232ABA1225730E100AD6A5C /* DeviceVerificationCoordinatorType.swift in Sources */, + B1C562D9228C0B760037F12A /* RoomContextualMenuItem.swift in Sources */, + B1C562E1228C7C8C0037F12A /* RoomContextualMenuToolbarView.swift in Sources */, B1B557BF20EF5B4500210D55 /* DisabledRoomInputToolbarView.m in Sources */, B1B5578620EF564900210D55 /* GroupTableViewCellWithSwitch.m in Sources */, B1098BE821ECFE52000DDA48 /* Coordinator.swift in Sources */, @@ -4036,6 +4091,7 @@ B1098C0021ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.swift in Sources */, B1B5591020EF782800210D55 /* TableViewCellWithPhoneNumberTextField.m in Sources */, B1DB4F06223015080065DBFA /* Character.swift in Sources */, + B1C562E8228C7CF20037F12A /* ContextualMenuItemView.swift in Sources */, B14F143022144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift in Sources */, B1E5368921FB1E20001F3AFF /* UIButton.swift in Sources */, ); diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 65b7ffdd3..bc8c22bdd 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -2555,8 +2555,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Get modular widget events in rooms histories [[MXKAppSettings standardAppSettings] addSupportedEventTypes:@[kWidgetMatrixEventTypeString, kWidgetModularEventTypeString]]; - // Disable long press on event in bubble cells - [MXKRoomBubbleTableViewCell disableLongPressGestureOnEvent:YES]; + // Enable long press on event in bubble cells + [MXKRoomBubbleTableViewCell disableLongPressGestureOnEvent:NO]; // Set first RoomDataSource class used in Vector [MXKRoomDataSourceManager registerRoomDataSourceClass:RoomDataSource.class]; diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/Contents.json new file mode 100644 index 000000000..a5230d6fe --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "room_context_menu_copy.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_copy@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_copy@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy.png new file mode 100644 index 000000000..7bf0c6e82 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy@2x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy@2x.png new file mode 100644 index 000000000..853e03326 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy@3x.png new file mode 100644 index 000000000..15363a0b0 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_copy.imageset/room_context_menu_copy@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/Contents.json new file mode 100644 index 000000000..1290bc2c4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "room_context_menu_edit.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_edit@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_edit@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit.png new file mode 100644 index 000000000..208378c76 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit@2x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit@2x.png new file mode 100644 index 000000000..4b9f38f01 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit@3x.png new file mode 100644 index 000000000..45034e075 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_edit.imageset/room_context_menu_edit@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/Contents.json new file mode 100644 index 000000000..aba738f89 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "room_context_menu_more.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_more@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_more@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more.png new file mode 100644 index 000000000..bb4a70d8a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more@2x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more@2x.png new file mode 100644 index 000000000..bedd0c99c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more@3x.png new file mode 100644 index 000000000..b7eb52e56 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_more.imageset/room_context_menu_more@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/Contents.json new file mode 100644 index 000000000..caedf0179 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "room_context_menu_reply.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_reply@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "room_context_menu_reply@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply.png new file mode 100644 index 000000000..da8c8d608 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply@2x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply@2x.png new file mode 100644 index 000000000..3767d9b0c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply@3x.png new file mode 100644 index 000000000..a7f162b4a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply.imageset/room_context_menu_reply@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 9e580c24f..75c74b9df 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -278,6 +278,8 @@ "room_event_action_cancel_send" = "Cancel Send"; "room_event_action_cancel_download" = "Cancel Download"; "room_event_action_view_encryption" = "Encryption Information"; +"room_event_action_reply" = "Reply"; +"room_event_action_edit" = "Edit"; "room_warning_about_encryption" = "End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption."; "room_event_failed_to_send" = "Failed to send"; "room_action_send_photo_or_video" = "Send photo or video"; diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h index 5162457f2..dade6d2ee 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h @@ -51,6 +51,15 @@ extern NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer; */ - (void)selectComponent:(NSUInteger)componentIndex; +/** + Highlight a component in receiver and show or not edit button. + + @param componentIndex index of the component in bubble message data + @param showEditButton true to show edit button + @param showTimestamp true to show timestamp label + */ +- (void)selectComponent:(NSUInteger)componentIndex showEditButton:(BOOL)showEditButton showTimestamp:(BOOL)showTimestamp; + /** Mark a component in receiver. diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 2e025b1ac..019756683 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -137,11 +137,19 @@ NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer = @"kMXKRoomBubbleCellT } - (void)selectComponent:(NSUInteger)componentIndex +{ + [self selectComponent:componentIndex showEditButton:YES showTimestamp:YES]; +} + +- (void)selectComponent:(NSUInteger)componentIndex showEditButton:(BOOL)showEditButton showTimestamp:(BOOL)showTimestamp { if (componentIndex < bubbleData.bubbleComponents.count) { - // Add time label - [self addTimestampLabelForComponent:componentIndex]; + if (showTimestamp) + { + // Add time label + [self addTimestampLabelForComponent:componentIndex]; + } // Blur timestamp labels which are not related to the selected component (if any) for (UIView* view in self.bubbleInfoContainer.subviews) @@ -164,8 +172,11 @@ NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer = @"kMXKRoomBubbleCellT } } - // Add the edit button - [self addEditButtonForComponent:componentIndex completion:nil]; + if (showEditButton) + { + // Add the edit button + [self addEditButtonForComponent:componentIndex completion:nil]; + } } } diff --git a/Riot/Categories/UIGestureRecognizer.swift b/Riot/Categories/UIGestureRecognizer.swift new file mode 100644 index 000000000..26dd05082 --- /dev/null +++ b/Riot/Categories/UIGestureRecognizer.swift @@ -0,0 +1,28 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +extension UIGestureRecognizer { + + func vc_isTouchingInside(view: UIView? = nil) -> Bool { + guard let view = view ?? self.view else { + return false + } + let touchedLocation = self.location(in: view) + return view.bounds.contains(touchedLocation) + } +} diff --git a/Riot/Categories/UIImage.swift b/Riot/Categories/UIImage.swift new file mode 100644 index 000000000..9c6847645 --- /dev/null +++ b/Riot/Categories/UIImage.swift @@ -0,0 +1,51 @@ +/* + Copyright 2019 New Vector 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 Foundation + +extension UIImage { + + class func vc_image(from color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage? { + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext() + + context?.setFillColor(color.cgColor) + context?.fill(rect) + + var image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + UIGraphicsBeginImageContext(size) + image?.draw(in: rect) + image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image + } + + func vc_tintedImage(usingColor tintColor: UIColor) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + let drawRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) + + self.draw(in: drawRect) + tintColor.set() + UIRectFillUsingBlendMode(drawRect, .sourceAtop) + let tintedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return tintedImage + } +} diff --git a/Riot/Categories/UIStackView.swift b/Riot/Categories/UIStackView.swift new file mode 100644 index 000000000..e1ebb800b --- /dev/null +++ b/Riot/Categories/UIStackView.swift @@ -0,0 +1,28 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +extension UIStackView { + + func vc_removeAllSubviews() { + let subviews = self.arrangedSubviews + for subview in subviews { + self.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + } +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 486035dc0..5007a2609 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -75,6 +75,10 @@ internal enum Asset { internal static let scrolldown = ImageAsset(name: "scrolldown") internal static let scrollup = ImageAsset(name: "scrollup") internal static let typing = ImageAsset(name: "typing") + internal static let roomContextMenuCopy = ImageAsset(name: "room_context_menu_copy") + internal static let roomContextMenuEdit = ImageAsset(name: "room_context_menu_edit") + internal static let roomContextMenuMore = ImageAsset(name: "room_context_menu_more") + internal static let roomContextMenuReply = ImageAsset(name: "room_context_menu_reply") internal static let uploadIcon = ImageAsset(name: "upload_icon") internal static let voiceCallIcon = ImageAsset(name: "voice_call_icon") internal static let addParticipant = ImageAsset(name: "add_participant") diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index 5db46ea18..b2aa4fda8 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -72,6 +72,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: KeyBackupSetupSuccessFromRecoveryKeyViewController.self) } + internal enum RoomContextualMenuViewController: StoryboardType { + internal static let storyboardName = "RoomContextualMenuViewController" + + internal static let initialScene = InitialSceneType(storyboard: RoomContextualMenuViewController.self) + } internal enum SimpleScreenTemplateViewController: StoryboardType { internal static let storyboardName = "SimpleScreenTemplateViewController" diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5932c9364..59c64bda3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1698,6 +1698,10 @@ internal enum VectorL10n { internal static var roomEventActionDelete: String { return VectorL10n.tr("Vector", "room_event_action_delete") } + /// Edit + internal static var roomEventActionEdit: String { + return VectorL10n.tr("Vector", "room_event_action_edit") + } /// Reason for kicking this user internal static var roomEventActionKickPromptReason: String { return VectorL10n.tr("Vector", "room_event_action_kick_prompt_reason") @@ -1718,6 +1722,10 @@ internal enum VectorL10n { internal static var roomEventActionRedact: String { return VectorL10n.tr("Vector", "room_event_action_redact") } + /// Reply + internal static var roomEventActionReply: String { + return VectorL10n.tr("Vector", "room_event_action_reply") + } /// Report content internal static var roomEventActionReport: String { return VectorL10n.tr("Vector", "room_event_action_report") diff --git a/Riot/Managers/Theme/Themable.swift b/Riot/Managers/Theme/Themable.swift index 3764b24b7..f4f59f1e9 100644 --- a/Riot/Managers/Theme/Themable.swift +++ b/Riot/Managers/Theme/Themable.swift @@ -16,6 +16,6 @@ import Foundation -protocol Themable: class { +@objc protocol Themable: class { func update(theme: Theme) } diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index dc59bf3a6..922fea039 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -35,6 +35,10 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic) BOOL containsLastMessage; +/** + Indicate true to display the timestamp of the selected component. + */ +@property(nonatomic) BOOL showTimestampForSelectedComponent; /** The event id of the current selected event inside the bubble. Default is nil. diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 81cbef55f..2b953e464 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -199,7 +199,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; } // Check whether the timestamp is displayed for this component, and check whether a vertical whitespace is required - if ((selectedComponentIndex == index || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + if (((selectedComponentIndex == index && self.showTimestampForSelectedComponent) || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) { currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; [currentAttributedTextMsg appendAttributedString:componentString]; @@ -238,7 +238,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; } // Check whether the timestamp is displayed - if (selectedComponentIndex == index || lastMessageIndex == index) + if ((selectedComponentIndex == index && self.showTimestampForSelectedComponent) || lastMessageIndex == index) { [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; } @@ -294,7 +294,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; // Check whether the timestamp is displayed for this first component, and check whether a vertical whitespace is required - if ((selectedComponentIndex == index || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + if (((selectedComponentIndex == index && self.showTimestampForSelectedComponent) || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) { attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; [attributedString appendAttributedString:component.attributedTextMessage]; @@ -322,7 +322,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; { // Prepare its attributed string by considering potential vertical margin required to display timestamp. NSAttributedString *componentString; - if (selectedComponentIndex == index || lastMessageIndex == index) + if ((selectedComponentIndex == index && self.showTimestampForSelectedComponent) || lastMessageIndex == index) { NSMutableAttributedString *componentAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; [componentAttributedString appendAttributedString:component.attributedTextMessage]; diff --git a/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift new file mode 100644 index 000000000..a76537140 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift @@ -0,0 +1,172 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import Reusable + +final class ContextualMenuItemView: UIView, NibOwnerLoadable { + + // MARK: - Constants + + private enum ColorAlpha { + static let normal: CGFloat = 1.0 + static let highlighted: CGFloat = 0.3 + } + + private enum ViewAlpha { + static let normal: CGFloat = 1.0 + static let disabled: CGFloat = 0.5 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + + // MARK: Private + + private var originalImage: UIImage? + + private var isHighlighted: Bool = false { + didSet { + self.updateView() + } + } + + // MARK: Public + + var titleColor: UIColor = .black { + didSet { + self.updateView() + } + } + + var imageColor: UIColor = .black { + didSet { + self.updateView() + } + } + + var isEnabled: Bool = true { + didSet { + self.updateView() + } + } + + var action: (() -> Void)? + + // MARK: Setup + + private func commonInit() { + self.setupGestureRecognizer() + } + + convenience init() { + self.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + self.commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + self.commonInit() + } + + // MARK: - Public + + func fill(title: String, image: UIImage?) { + self.originalImage = image?.withRenderingMode(.alwaysTemplate) + self.titleLabel.text = title + self.updateView() + } + + func fill(menuItem: RoomContextualMenuItem) { + self.fill(title: menuItem.title, image: menuItem.image) + self.action = menuItem.action + self.isEnabled = menuItem.isEnabled + } + + // MARK: - Private + + private func setupGestureRecognizer() { + let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(buttonAction(_:))) + gestureRecognizer.minimumPressDuration = 0 + self.addGestureRecognizer(gestureRecognizer) + } + + private func updateView() { + + let viewAlpha = self.isEnabled ? ViewAlpha.normal : ViewAlpha.disabled + let colorAlpha = self.isHighlighted ? ColorAlpha.highlighted : ColorAlpha.normal + + self.updateTitleAndImageAlpha(viewAlpha) + self.imageView.tintColor = self.imageColor + self.updateTitleAndImageColorAlpha(colorAlpha) + } + + private func updateTitleAndImageAlpha(_ alpha: CGFloat) { + self.imageView.alpha = alpha + self.titleLabel.alpha = alpha + } + + private func updateTitleAndImageColorAlpha(_ alpha: CGFloat) { + let titleColor: UIColor + let image: UIImage? + + if alpha < 1.0 { + titleColor = self.titleColor.withAlphaComponent(alpha) + image = self.originalImage?.vc_tintedImage(usingColor: self.imageColor.withAlphaComponent(alpha)) + } else { + titleColor = self.titleColor + image = self.originalImage + } + + self.titleLabel.textColor = titleColor + self.imageView.image = image + } + + // MARK: - Actions + + @objc private func buttonAction(_ sender: UILongPressGestureRecognizer) { + guard self.isEnabled else { + return + } + + let isBackgroundViewTouched = sender.vc_isTouchingInside() + + switch sender.state { + case .began, .changed: + self.isHighlighted = isBackgroundViewTouched + case .ended: + self.isHighlighted = false + + if isBackgroundViewTouched { + self.action?() + } + case .cancelled: + self.isHighlighted = false + default: + break + } + } +} diff --git a/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.xib b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.xib new file mode 100644 index 000000000..9c2ce9ac5 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift new file mode 100644 index 000000000..f4898fca0 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift @@ -0,0 +1,62 @@ +/* + Copyright 2019 New Vector 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 Foundation + +@objc enum RoomContextualMenuAction: Int { + case copy + case reply + case edit + case more + + // MARK: - Properties + + var title: String { + let title: String + + switch self { + case .copy: + title = VectorL10n.roomEventActionCopy + case .reply: + title = VectorL10n.roomEventActionReply + case .edit: + title = VectorL10n.roomEventActionEdit + case .more: + title = VectorL10n.roomEventActionMore + } + + return title + } + + var image: UIImage? { + let image: UIImage? + + switch self { + case .copy: + image = Asset.Images.roomContextMenuCopy.image + case .reply: + image = Asset.Images.roomContextMenuReply.image + case .edit: + image = Asset.Images.roomContextMenuEdit.image + case .more: + image = Asset.Images.roomContextMenuMore.image + default: + image = nil + } + + return image + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuItem.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuItem.swift new file mode 100644 index 000000000..ba32691f3 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuItem.swift @@ -0,0 +1,37 @@ +/* + Copyright 2019 New Vector 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 Foundation + +@objcMembers +final class RoomContextualMenuItem: NSObject { + + // MARK: - Properties + + let title: String + let image: UIImage? + + var isEnabled: Bool = true + var action: (() -> Void)? + + // MARK: - Setup + + init(menuAction: RoomContextualMenuAction) { + self.title = menuAction.title + self.image = menuAction.image + super.init() + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuPresenter.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuPresenter.swift new file mode 100644 index 000000000..b066de8eb --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuPresenter.swift @@ -0,0 +1,105 @@ +/* + Copyright 2019 New Vector 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 Foundation + +@objcMembers +final class RoomContextualMenuPresenter: NSObject { + + // MARK: - Constants + + private enum Constants { + static let animationDuration: TimeInterval = 0.3 + } + + // MARK: - Properties + + // MARK: Private + + private weak var roomContextualMenuViewController: RoomContextualMenuViewController? + + // MARK: Public + + var isPresenting: Bool { + return self.roomContextualMenuViewController != nil + } + + // MARK: - Public + + func present(roomContextualMenuViewController: RoomContextualMenuViewController, + from viewController: UIViewController, + on view: UIView, + animated: Bool, + completion: (() -> Void)?) { + guard self.roomContextualMenuViewController == nil else { + return + } + + roomContextualMenuViewController.view.alpha = 0 + + viewController.vc_addChildViewController(viewController: roomContextualMenuViewController, onView: view) + + self.roomContextualMenuViewController = roomContextualMenuViewController + + roomContextualMenuViewController.hideMenuToolbar() + roomContextualMenuViewController.view.layoutIfNeeded() + + let animationInstructions: (() -> Void) = { + roomContextualMenuViewController.showMenuToolbar() + roomContextualMenuViewController.view.alpha = 1 + roomContextualMenuViewController.view.layoutIfNeeded() + } + + if animated { + UIView.animate(withDuration: Constants.animationDuration, animations: { + animationInstructions() + }, completion: { completed in + completion?() + }) + } else { + animationInstructions() + completion?() + } + } + + func hideContextualMenu(animated: Bool, completion: (() -> Void)?) { + guard let roomContextualMenuViewController = self.roomContextualMenuViewController else { + return + } + + let animationInstructions: (() -> Void) = { + roomContextualMenuViewController.hideMenuToolbar() + roomContextualMenuViewController.view.alpha = 0 + roomContextualMenuViewController.view.layoutIfNeeded() + } + + let animationCompletionInstructions: (() -> Void) = { + roomContextualMenuViewController.vc_removeFromParent() + completion?() + } + + if animated { + UIView.animate(withDuration: Constants.animationDuration, animations: { + animationInstructions() + }, completion: { completed in + animationCompletionInstructions() + }) + } else { + animationInstructions() + animationCompletionInstructions() + } + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.swift new file mode 100644 index 000000000..c4c389372 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.swift @@ -0,0 +1,141 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import Reusable + +final class RoomContextualMenuToolbarView: MXKRoomInputToolbarView, NibOwnerLoadable, Themable { + + // MARK: - Constants + + private enum Constants { + static let menuItemMinWidth: CGFloat = 50.0 + static let menuItemMaxWidth: CGFloat = 80.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var menuItemsStackView: UIStackView! + @IBOutlet private weak var separatorView: UIView! + + // MARK: Private + + private var theme: Theme? + private var menuItemViews: [ContextualMenuItemView] = [] + + // MARK: - Public + + @objc func update(theme: Theme) { + self.theme = theme + self.backgroundColor = theme.backgroundColor + self.tintColor = theme.tintColor + self.separatorView.backgroundColor = theme.lineBreakColor + + for menuItemView in self.menuItemViews { + menuItemView.titleColor = theme.textPrimaryColor + menuItemView.imageColor = theme.tintColor + } + } + + @objc func fill(contextualMenuItems: [RoomContextualMenuItem]) { + self.menuItemsStackView.vc_removeAllSubviews() + self.menuItemViews.removeAll() + + for menuItem in contextualMenuItems { + let menuItemView = ContextualMenuItemView() + menuItemView.fill(menuItem: menuItem) + + if let theme = theme { + menuItemView.titleColor = theme.textPrimaryColor + menuItemView.imageColor = theme.tintColor + } + + self.add(menuItemView: menuItemView) + } + + self.layoutIfNeeded() + } + + // MARK: - Setup + + private func commonInit() { + } + + convenience init() { + self.init(frame: CGRect.zero) + self.loadNibContent() + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + commonInit() + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + } + + // MARK: - Private + + private func add(menuItemView: ContextualMenuItemView) { + let menuItemContentView = UIView() + menuItemContentView.backgroundColor = .clear + + self.add(menuItemView: menuItemView, on: menuItemContentView) + + self.menuItemsStackView.addArrangedSubview(menuItemContentView) + + let widthConstraint = menuItemContentView.widthAnchor.constraint(equalTo: self.menuItemsStackView.widthAnchor) + widthConstraint.priority = .defaultLow + widthConstraint.isActive = true + + self.menuItemViews.append(menuItemView) + } + + private func add(menuItemView: ContextualMenuItemView, on contentView: UIView) { + contentView.translatesAutoresizingMaskIntoConstraints = false + menuItemView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(menuItemView) + + menuItemView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true + menuItemView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true + + let widthConstraint = menuItemView.widthAnchor.constraint(equalToConstant: 0.0) + widthConstraint.priority = .defaultLow + widthConstraint.isActive = true + + let minWidthConstraint = menuItemView.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.menuItemMinWidth) + minWidthConstraint.priority = .required + minWidthConstraint.isActive = true + + let maxWidthConstraint = menuItemView.widthAnchor.constraint(lessThanOrEqualToConstant: Constants.menuItemMaxWidth) + maxWidthConstraint.priority = .required + maxWidthConstraint.isActive = true + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.xib b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.xib new file mode 100644 index 000000000..42b2b9ab3 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.storyboard b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.storyboard new file mode 100644 index 000000000..a06eecc81 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.storyboard @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift new file mode 100644 index 000000000..7492a1e5e --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift @@ -0,0 +1,113 @@ +/* + Copyright 2019 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +@objc protocol RoomContextualMenuViewControllerDelegate: class { + func roomContextualMenuViewControllerDidTapBackgroundOverlay(_ viewController: RoomContextualMenuViewController) +} + +@objcMembers +final class RoomContextualMenuViewController: UIViewController, Themable { + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var backgroundOverlayView: UIView! + @IBOutlet private weak var menuToolbarView: RoomContextualMenuToolbarView! + + @IBOutlet private weak var menuToolbarViewHeightConstraint: NSLayoutConstraint! + @IBOutlet private weak var menuToolbarViewBottomConstraint: NSLayoutConstraint! + + // MARK: Private + + private var theme: Theme! + private var contextualMenuItems: [RoomContextualMenuItem] = [] + + private var hiddenToolbarViewBottomConstant: CGFloat { + let bottomSafeAreaHeight: CGFloat + + if #available(iOS 11.0, *) { + bottomSafeAreaHeight = self.view.safeAreaInsets.bottom + } else { + bottomSafeAreaHeight = self.bottomLayoutGuide.length + } + + return -(self.menuToolbarViewHeightConstraint.constant + bottomSafeAreaHeight) + } + + // MARK: Public + + weak var delegate: RoomContextualMenuViewControllerDelegate? + + // MARK: - Setup + + class func instantiate(with contextualMenuItems: [RoomContextualMenuItem]) -> RoomContextualMenuViewController { + let viewController = StoryboardScene.RoomContextualMenuViewController.initialScene.instantiate() + viewController.theme = ThemeService.shared().theme + viewController.contextualMenuItems = contextualMenuItems + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + self.backgroundOverlayView.isUserInteractionEnabled = true + self.menuToolbarView.fill(contextualMenuItems: self.contextualMenuItems) + self.setupBackgroundOverlayTapGestureRecognizer() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + } + + // MARK: - Public + + func showMenuToolbar() { + self.menuToolbarViewBottomConstraint.constant = 0 + } + + func hideMenuToolbar() { + self.menuToolbarViewBottomConstraint.constant = self.hiddenToolbarViewBottomConstant + } + + func update(theme: Theme) { + self.menuToolbarView.update(theme: theme) + } + + // MARK: - Private + + private func setupBackgroundOverlayTapGestureRecognizer() { + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(gestureRecognizer:))) + self.backgroundOverlayView.addGestureRecognizer(tapGestureRecognizer) + } + + @objc private func handleTap(gestureRecognizer: UIGestureRecognizer) { + self.delegate?.roomContextualMenuViewControllerDidTapBackgroundOverlay(self) + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } +} diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index dc695ba7c..cfc62b45d 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -34,6 +34,11 @@ */ @property(nonatomic) BOOL markTimelineInitialEvent; +/** + Tell whether timestamp should be displayed on event selection. Default is YES. + */ +@property(nonatomic) BOOL showBubbleDateTimeOnSelection; + /** Check if there is an active jitsi widget in the room and return it. diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 49d189283..c7f2e74aa 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -58,6 +58,8 @@ self.markTimelineInitialEvent = NO; + self.showBubbleDateTimeOnSelection = YES; + // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -445,7 +447,7 @@ NSInteger selectedComponentIndex = cellData.selectedComponentIndex; if (selectedComponentIndex != NSNotFound) { - [bubbleCell selectComponent:cellData.selectedComponentIndex]; + [bubbleCell selectComponent:cellData.selectedComponentIndex showEditButton:NO showTimestamp:cellData.showTimestampForSelectedComponent]; } else { @@ -492,11 +494,14 @@ { RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:_selectedEventId]; cellData.selectedEventId = nil; + cellData.showTimestampForSelectedComponent = NO; } if (selectedEventId.length) { RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:selectedEventId]; + + cellData.showTimestampForSelectedComponent = self.showBubbleDateTimeOnSelection; if (cellData.collapsed && cellData.nextCollapsableCellData) { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 97ec2831e..c81d636d6 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -123,7 +123,7 @@ #import "Riot-Swift.h" -@interface RoomViewController () +@interface RoomViewController () { // The expanded header ExpandedRoomTitleView *expandedHeader; @@ -213,6 +213,10 @@ MXServerNotices *serverNotices; } +@property (nonatomic, weak) IBOutlet UIView *overlayContainerView; + +@property (nonatomic, strong) RoomContextualMenuPresenter *roomContextualMenuPresenter; + @end @implementation RoomViewController @@ -404,6 +408,8 @@ [self refreshRoomInputToolbar]; } + self.roomContextualMenuPresenter = [RoomContextualMenuPresenter new]; + // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -589,6 +595,9 @@ { [super viewDidDisappear:animated]; + // Hide contextual menu if needed + [self hideContextualMenuAnimated:NO]; + // Reset visible room id [AppDelegate theDelegate].visibleRoomId = nil; @@ -936,6 +945,8 @@ - (void)updateRoomInputToolbarViewClassIfNeeded { Class roomInputToolbarViewClass = RoomInputToolbarView.class; + + BOOL shouldDismissContextualMenu = NO; // Check the user has enough power to post message if (self.roomDataSource.roomState) @@ -950,10 +961,12 @@ if (isRoomObsolete || isResourceLimitExceeded) { roomInputToolbarViewClass = nil; + shouldDismissContextualMenu = YES; } else if (!canSend) { roomInputToolbarViewClass = DisabledRoomInputToolbarView.class; + shouldDismissContextualMenu = YES; } } @@ -961,6 +974,12 @@ if (self.isRoomPreview) { roomInputToolbarViewClass = nil; + shouldDismissContextualMenu = YES; + } + + if (shouldDismissContextualMenu) + { + [self hideContextualMenuAnimated:NO]; } // Change inputToolbarView class only if given class is different from current one @@ -978,7 +997,7 @@ if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { - height = ((RoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant; + height = ((RoomInputToolbarView*)self.inputToolbarView).mainToolbarHeightConstraint.constant; } else if ([self.inputToolbarView isKindOfClass:DisabledRoomInputToolbarView.class]) { @@ -1485,6 +1504,14 @@ [UIView setAnimationsEnabled:YES]; } +- (void)handleLongPressFromCell:(id)cell withTappedEvent:(MXEvent*)event +{ + if (event && !customizedRoomDataSource.selectedEventId) + { + [self showContextualMenuForEvent:event cell:cell animated:YES]; + } +} + #pragma mark - Hide/Show expanded header - (void)showExpandedHeader:(BOOL)isVisible @@ -1552,6 +1579,9 @@ mainNavigationController.navigationBar.translucent = isVisible; self.navigationController.navigationBar.translucent = isVisible; + // Hide contextual menu if needed + [self hideContextualMenuAnimated:YES]; + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ @@ -2030,9 +2060,6 @@ [self selectEventWithId:tappedEvent.eventId]; } } - - // Force table refresh - [self dataSource:self.roomDataSource didCellChange:nil]; } else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnOverlayContainer]) { @@ -2073,9 +2100,6 @@ // Highlight this event in displayed message [self selectEventWithId:((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventId]; } - - // Force table refresh - [self dataSource:self.roomDataSource didCellChange:nil]; } else { @@ -2105,6 +2129,11 @@ [self.roomDataSource collapseRoomBubble:((MXKRoomBubbleTableViewCell*)cell).bubbleData collapsed:YES]; } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent]) + { + MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey]; + [self handleLongPressFromCell:cell withTappedEvent:tappedEvent]; + } else { // Keep default implementation for other actions @@ -2213,24 +2242,6 @@ }]]; } } - - if (level == 0) - { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_copy", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - [self cancelEventSelection]; - - [[UIPasteboard generalPasteboard] setString:selectedComponent.textMessage]; - } - - }]]; - } if (level == 0) { @@ -2323,42 +2334,6 @@ }]]; } - if (attachment.type != MXKAttachmentTypeSticker) - { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_copy", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - [self cancelEventSelection]; - - [self startActivityIndicator]; - - [attachment copy:^{ - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - } failure:^(NSError *error) { - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - //Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - - // Start animation in case of download during attachment preparing - [roomBubbleTableViewCell startProgressUI]; - } - - }]]; - } - // Check status of the selected event if (selectedEvent.sentState == MXEventSentStatePreparing || selectedEvent.sentState == MXEventSentStateEncrypting || @@ -2733,7 +2708,7 @@ if (weakSelf) { typeof(self) self = weakSelf; - [self cancelEventSelection]; + [self hideContextualMenuAnimated:YES]; } }]]; @@ -2843,10 +2818,8 @@ else if (url && urlItemInteractionValue) { // Fallback case for external links - - // TODO: Use UITextItemInteraction enum when minimum deployement target will be iOS 10 switch (urlItemInteractionValue.integerValue) { - case 0: //UITextItemInteractionInvokeDefaultAction + case UITextItemInteractionInvokeDefaultAction: { [[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) { if (!success) @@ -2857,10 +2830,13 @@ shouldDoAction = NO; } break; - case 1: //UITextItemInteractionPresentActions - // Long press on link, let MXKRoomBubbleTableViewCell UITextView present the default contextual menu. + case UITextItemInteractionPresentActions: + { + // Long press on link, present room contextual menu. + shouldDoAction = NO; + } break; - case 2: //UITextItemInteractionPreview + case UITextItemInteractionPreview: // Force touch on link, let MXKRoomBubbleTableViewCell UITextView use default peek and pop behavior. break; default: @@ -2878,11 +2854,18 @@ - (void)selectEventWithId:(NSString*)eventId { - BOOL shouldEnableReplyMode = [self.roomDataSource canReplyToEventWithId:eventId]; - - [self setInputToolBarSendMode: shouldEnableReplyMode ? RoomInputToolbarViewSendModeReply : RoomInputToolbarViewSendModeSend]; + [self selectEventWithId:eventId enableReplyMode:NO showTimestamp:YES]; +} +- (void)selectEventWithId:(NSString*)eventId enableReplyMode:(BOOL)enableReplyMode showTimestamp:(BOOL)showTimestamp +{ + [self setInputToolBarSendMode: enableReplyMode ? RoomInputToolbarViewSendModeReply : RoomInputToolbarViewSendModeSend]; + + customizedRoomDataSource.showBubbleDateTimeOnSelection = showTimestamp; customizedRoomDataSource.selectedEventId = eventId; + + // Force table refresh + [self dataSource:self.roomDataSource didCellChange:nil]; } - (void)cancelEventSelection @@ -2895,6 +2878,7 @@ currentAlert = nil; } + customizedRoomDataSource.showBubbleDateTimeOnSelection = YES; customizedRoomDataSource.selectedEventId = nil; // Force table refresh @@ -4976,5 +4960,169 @@ } } +#pragma mark - Contextual Menu + +- (NSArray*)contextualMenuItemsForEvent:(MXEvent*)event andCell:(id)cell +{ + NSString *eventId = event.eventId; + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; + MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment; + + MXWeakify(self); + + // Copy action + + RoomContextualMenuItem *copyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionCopy]; + copyMenuItem.isEnabled = !attachment || attachment.type != MXKAttachmentTypeSticker; + copyMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + + if (!attachment) + { + NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents; + MXKRoomBubbleComponent *selectedComponent; + for (selectedComponent in components) + { + if ([selectedComponent.event.eventId isEqualToString:event.eventId]) + { + break; + } + selectedComponent = nil; + } + NSString *textMessage = selectedComponent.textMessage; + + [UIPasteboard generalPasteboard].string = textMessage; + + [self hideContextualMenuAnimated:YES]; + } + else if (attachment.type != MXKAttachmentTypeSticker) + { + [self hideContextualMenuAnimated:YES completion:^{ + [self startActivityIndicator]; + + [attachment copy:^{ + + [self stopActivityIndicator]; + + } failure:^(NSError *error) { + + [self stopActivityIndicator]; + + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + + // Start animation in case of download during attachment preparing + [roomBubbleTableViewCell startProgressUI]; + }]; + } + }; + + // Reply action + + RoomContextualMenuItem *replyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReply]; + replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:eventId]; + replyMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + + [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; + [self selectEventWithId:eventId enableReplyMode:YES showTimestamp:NO]; + }; + + // Edit action + + RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit]; + // TODO: Handle edit action + editMenuItem.isEnabled = NO; + + // More action + + RoomContextualMenuItem *moreMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionMore]; + moreMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self showEditButtonAlertMenuForEvent:event inCell:cell level:0]; + }; + + // Actions list + + NSArray *actionItems = @[ + copyMenuItem, + replyMenuItem, + editMenuItem, + moreMenuItem + ]; + + return actionItems; +} + +- (void)showContextualMenuForEvent:(MXEvent*)event cell:(id)cell animated:(BOOL)animated +{ + if (self.roomContextualMenuPresenter.isPresenting) + { + return; + } + + [self selectEventWithId:event.eventId enableReplyMode:NO showTimestamp:NO]; + + NSArray* contextualMenuItems = [self contextualMenuItemsForEvent:event andCell:cell]; + + RoomContextualMenuViewController *roomContextualMenuViewController = [RoomContextualMenuViewController instantiateWith:contextualMenuItems]; + roomContextualMenuViewController.delegate = self; + + [self.roomContextualMenuPresenter presentWithRoomContextualMenuViewController:roomContextualMenuViewController + from:self + on:self.overlayContainerView + animated:YES + completion:^{ + [self contextualMenuAnimationCompletionAfterBeingShown:YES]; + }]; +} + +- (void)hideContextualMenuAnimated:(BOOL)animated +{ + [self hideContextualMenuAnimated:animated completion:nil]; +} + +- (void)hideContextualMenuAnimated:(BOOL)animated completion:(void(^)(void))completion +{ + [self hideContextualMenuAnimated:animated cancelEventSelection:YES completion:completion]; +} + +- (void)hideContextualMenuAnimated:(BOOL)animated cancelEventSelection:(BOOL)cancelEventSelection completion:(void(^)(void))completion +{ + if (!self.roomContextualMenuPresenter.isPresenting) + { + return; + } + + if (cancelEventSelection) + { + [self cancelEventSelection]; + } + + [self.roomContextualMenuPresenter hideContextualMenuWithAnimated:animated completion:^{ + [self contextualMenuAnimationCompletionAfterBeingShown:NO]; + + if (completion) + { + completion(); + } + }]; +} + +- (void)contextualMenuAnimationCompletionAfterBeingShown:(BOOL)isShown +{ + self.inputToolbarView.editable = !isShown; + self.bubblesTableView.scrollsToTop = !isShown; + self.overlayContainerView.userInteractionEnabled = isShown; +} + +#pragma mark - RoomContextualMenuViewControllerDelegate + +- (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController +{ + [self hideContextualMenuAnimated:YES]; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 0a8a14425..6ea9ec67f 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,11 +1,11 @@ - + - + @@ -21,6 +21,7 @@ + @@ -130,7 +131,7 @@ - + @@ -146,10 +147,15 @@ + + + + + @@ -160,12 +166,15 @@ + + + @@ -174,7 +183,7 @@ - - + +