diff --git a/CHANGES.rst b/CHANGES.rst index 565ba5ebb..6a65585fa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changes in 0.8.7 (2019-xx-xx) Improvements: * RoomVC: When replying, use a "Reply" button instead of "Send". * RoomVC: New message actions (#2394). + * Reactions: Display existing reactions below the message (#2396). Changes in 0.8.6 (2019-05-06) =============================================== diff --git a/Podfile b/Podfile index 280ff6c64..59ed9a2fe 100644 --- a/Podfile +++ b/Podfile @@ -79,6 +79,7 @@ abstract_target 'RiotPods' do target "Riot" do import_MatrixKit + pod 'DGCollectionViewLeftAlignFlowLayout', '~> 1.0.4' end target "RiotShareExtension" do diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 3bc44f0de..542e7eb2d 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -194,6 +194,14 @@ B169331720F3CBE000746532 /* RecentCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = B16932F920F3C51900746532 /* RecentCellData.m */; }; B17982FF2119FED2001FD722 /* GDPRConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17982FE2119FED2001FD722 /* GDPRConsentViewController.swift */; }; B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1798301211B13B3001FD722 /* OnBoardingManager.swift */; }; + B1963B2B228F1C4900CBA17F /* BubbleReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B25228F1C4800CBA17F /* BubbleReactionsView.swift */; }; + B1963B2C228F1C4900CBA17F /* BubbleReactionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1963B26228F1C4800CBA17F /* BubbleReactionViewCell.xib */; }; + B1963B2D228F1C4900CBA17F /* BubbleReactionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B27228F1C4800CBA17F /* BubbleReactionsViewModel.swift */; }; + B1963B2E228F1C4900CBA17F /* BubbleReactionViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B28228F1C4800CBA17F /* BubbleReactionViewData.swift */; }; + B1963B2F228F1C4900CBA17F /* BubbleReactionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B29228F1C4800CBA17F /* BubbleReactionViewCell.swift */; }; + B1963B30228F1C4900CBA17F /* BubbleReactionsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1963B2A228F1C4800CBA17F /* BubbleReactionsView.xib */; }; + B1963B32228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B31228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift */; }; + B1963B3822933BC800CBA17F /* AutosizedCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B3722933BC800CBA17F /* AutosizedCollectionView.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 */; }; @@ -784,6 +792,14 @@ B169331320F3CAFC00746532 /* PublicRoomsDirectoryDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PublicRoomsDirectoryDataSource.h; sourceTree = ""; }; B17982FE2119FED2001FD722 /* GDPRConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GDPRConsentViewController.swift; sourceTree = ""; }; B1798301211B13B3001FD722 /* OnBoardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingManager.swift; sourceTree = ""; }; + B1963B25228F1C4800CBA17F /* BubbleReactionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionsView.swift; sourceTree = ""; }; + B1963B26228F1C4800CBA17F /* BubbleReactionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BubbleReactionViewCell.xib; sourceTree = ""; }; + B1963B27228F1C4800CBA17F /* BubbleReactionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionsViewModel.swift; sourceTree = ""; }; + B1963B28228F1C4800CBA17F /* BubbleReactionViewData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionViewData.swift; sourceTree = ""; }; + B1963B29228F1C4800CBA17F /* BubbleReactionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionViewCell.swift; sourceTree = ""; }; + B1963B2A228F1C4800CBA17F /* BubbleReactionsView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BubbleReactionsView.xib; sourceTree = ""; }; + B1963B31228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleReactionsViewModelType.swift; sourceTree = ""; }; + B1963B3722933BC800CBA17F /* AutosizedCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutosizedCollectionView.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 = ""; }; @@ -1936,6 +1952,29 @@ path = OnBoarding; sourceTree = ""; }; + B1963B24228F1C4800CBA17F /* BubbleReactions */ = { + isa = PBXGroup; + children = ( + B1963B31228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift */, + B1963B27228F1C4800CBA17F /* BubbleReactionsViewModel.swift */, + B1963B25228F1C4800CBA17F /* BubbleReactionsView.swift */, + B1963B2A228F1C4800CBA17F /* BubbleReactionsView.xib */, + B1963B28228F1C4800CBA17F /* BubbleReactionViewData.swift */, + B1963B29228F1C4800CBA17F /* BubbleReactionViewCell.swift */, + B1963B26228F1C4800CBA17F /* BubbleReactionViewCell.xib */, + ); + path = BubbleReactions; + sourceTree = ""; + }; + B1963B3622933B9500CBA17F /* CollectionView */ = { + isa = PBXGroup; + children = ( + B1963B3722933BC800CBA17F /* AutosizedCollectionView.swift */, + ); + name = CollectionView; + path = Riot/Modules/Common/CollectionView; + sourceTree = SOURCE_ROOT; + }; B1B5567620EE6C4C00210D55 /* Modules */ = { isa = PBXGroup; children = ( @@ -2050,6 +2089,7 @@ B1B556A420EE6C4C00210D55 /* Members */, B1B5569020EE6C4C00210D55 /* Settings */, B1C562D7228C0B4C0037F12A /* ContextualMenu */, + B1963B24228F1C4800CBA17F /* BubbleReactions */, ); path = Room; sourceTree = ""; @@ -2231,6 +2271,7 @@ B1B556CD20EE6C4C00210D55 /* Common */ = { isa = PBXGroup; children = ( + B1963B3622933B9500CBA17F /* CollectionView */, B1B556CE20EE6C4C00210D55 /* WebViewController */, B1B556D120EE6C4C00210D55 /* NavigationController */, B1B556D420EE6C4C00210D55 /* SegmentedViewController */, @@ -3537,6 +3578,7 @@ B1B5593920EF7BAC00210D55 /* TableViewCellWithCheckBoxes.xib in Resources */, B1B557C120EF5B4500210D55 /* DisabledRoomInputToolbarView.xib in Resources */, 32891D6C2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard in Resources */, + B1963B2C228F1C4900CBA17F /* BubbleReactionViewCell.xib in Resources */, B1664DA320F4F96200808783 /* Vector.strings in Resources */, B1B557C720EF5CD400210D55 /* DirectoryServerDetailTableViewCell.xib in Resources */, B1B5582620EF638A00210D55 /* RoomMemberTitleView.xib in Resources */, @@ -3588,6 +3630,7 @@ B1B5590F20EF782800210D55 /* TableViewCellWithPhoneNumberTextField.xib in Resources */, B1B5578520EF564900210D55 /* GroupTableViewCellWithSwitch.xib in Resources */, B1B557B320EF5AEF00210D55 /* EventDetailsView.xib in Resources */, + B1963B30228F1C4900CBA17F /* BubbleReactionsView.xib in Resources */, B1B557DD20EF5FBB00210D55 /* FilesSearchTableViewCell.xib in Resources */, B1B5590320EF768F00210D55 /* RoomSelectedStickerBubbleCell.xib in Resources */, 3232ABB62257BE6400AD6A5C /* DeviceVerificationVerifyViewController.storyboard in Resources */, @@ -3634,6 +3677,7 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RiotPods-Riot/Pods-RiotPods-Riot-frameworks.sh", "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework", + "${BUILT_PRODUCTS_DIR}/DGCollectionViewLeftAlignFlowLayout/DGCollectionViewLeftAlignFlowLayout.framework", "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework", "${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework", "${BUILT_PRODUCTS_DIR}/GBDeviceInfo/GBDeviceInfo.framework", @@ -3659,6 +3703,7 @@ ); outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AFNetworking.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DGCollectionViewLeftAlignFlowLayout.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTFoundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GBDeviceInfo.framework", @@ -3882,6 +3927,7 @@ B1098BDF21ECE09F000DDA48 /* Strings.swift in Sources */, B1B558C420EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, 3232ABC022594C0900AD6A5C /* VerifyEmojiCollectionViewCell.swift in Sources */, + B1963B2E228F1C4900CBA17F /* BubbleReactionViewData.swift in Sources */, B1B5572F20EE6C4D00210D55 /* ReadReceiptsViewController.m in Sources */, B1B558CB20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, B169330B20F3CA3A00746532 /* Contact.m in Sources */, @@ -3914,6 +3960,7 @@ B1B5593B20EF7BAC00210D55 /* TableViewCellWithCheckBoxAndLabel.m in Sources */, B1B5581A20EF625800210D55 /* ExpandedRoomTitleView.m in Sources */, B1107EC82200B0720038014B /* KeyBackupRecoverSuccessViewController.swift in Sources */, + B1963B2F228F1C4900CBA17F /* BubbleReactionViewCell.swift in Sources */, B1B558E920EF768F00210D55 /* RoomSelectedStickerBubbleCell.m in Sources */, B1B558DF20EF768F00210D55 /* RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, F083BE041E7009ED00A9B29C /* Tools.m in Sources */, @@ -3949,6 +3996,7 @@ 32F6B96E2270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift in Sources */, B1B5592C20EF7A5D00210D55 /* TableViewCellWithButton.m in Sources */, 32242F1421E8FBA900725742 /* DefaultTheme.swift in Sources */, + B1963B2D228F1C4900CBA17F /* BubbleReactionsViewModel.swift in Sources */, 32242F1321E8FBA900725742 /* Theme.swift in Sources */, B1B5582520EF638A00210D55 /* RoomMemberTitleView.m in Sources */, B1B5582C20EF666100210D55 /* DirectoryRecentTableViewCell.m in Sources */, @@ -4029,6 +4077,7 @@ B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */, B1B5573520EE6C4D00210D55 /* GroupDetailsViewController.m in Sources */, B10B3B5B2201DD740072C76B /* KeyBackupBannerCell.swift in Sources */, + B1963B32228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift in Sources */, B1098BFA21ECFE65000DDA48 /* KeyBackupSetupPassphraseViewModel.swift in Sources */, B1B5575220EE6C4D00210D55 /* RoomKeyRequestViewController.m in Sources */, F083BD1E1E7009ED00A9B29C /* AppDelegate.m in Sources */, @@ -4082,8 +4131,10 @@ B110872621F098F0003554A5 /* ActivityIndicatorView.swift in Sources */, B19EFA3921F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift in Sources */, B1E5368D21FB7245001F3AFF /* KeyBackupRecoverFromPassphraseViewController.swift in Sources */, + B1963B3822933BC800CBA17F /* AutosizedCollectionView.swift in Sources */, B169330320F3C98900746532 /* RoomBubbleCellData.m in Sources */, B1B557CC20EF5D8000210D55 /* DirectoryServerTableViewCell.m in Sources */, + B1963B2B228F1C4900CBA17F /* BubbleReactionsView.swift in Sources */, B1B5575C20EE6C4D00210D55 /* DirectoryViewController.m in Sources */, B1B558BD20EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.m in Sources */, B1B5577020EE702800210D55 /* WidgetPickerViewController.m in Sources */, diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 3c939e2c1..c98b27a24 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -30,8 +30,8 @@ class DarkTheme: NSObject, Theme { var searchBackgroundColor: UIColor = UIColor(rgb: 0x181B21) var searchPlaceholderColor: UIColor = UIColor(rgb: 0x61708B) - var headerBackgroundColor: UIColor = UIColor(rgb: 0x15171B) - var headerBorderColor: UIColor = UIColor(rgb: 0x22262E) + var headerBackgroundColor: UIColor = UIColor(rgb: 0x22262E) + var headerBorderColor: UIColor = UIColor(rgb: 0x181B21) var headerTextPrimaryColor: UIColor = UIColor(rgb: 0xA1B2D1) var headerTextSecondaryColor: UIColor = UIColor(rgb: 0xC8C8CD) @@ -39,7 +39,7 @@ class DarkTheme: NSObject, Theme { var textSecondaryColor: UIColor = UIColor(rgb: 0xA1B2D1) var tintColor: UIColor = UIColor(rgb: 0x03B381) - var tintBackgroundColor: UIColor = UIColor(rgb: 0xe9fff9) + var tintBackgroundColor: UIColor = UIColor(rgb: 0x1F6954) var unreadRoomIndentColor: UIColor = UIColor(rgb: 0x2E3648) var lineBreakColor: UIColor = UIColor(rgb: 0x61708B) diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index 19b1ba109..7f154f232 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -30,7 +30,7 @@ class DefaultTheme: NSObject, Theme { var searchBackgroundColor: UIColor = UIColor(rgb: 0xFFFFFF) var searchPlaceholderColor: UIColor = UIColor(rgb: 0x61708B) - var headerBackgroundColor: UIColor = UIColor(rgb: 0xF2F5F8) + var headerBackgroundColor: UIColor = UIColor(rgb: 0xF3F8FD) var headerBorderColor: UIColor = UIColor(rgb: 0xE9EDF1) var headerTextPrimaryColor: UIColor = UIColor(rgb: 0x61708B) var headerTextSecondaryColor: UIColor = UIColor(rgb: 0xC8C8CD) diff --git a/Riot/Modules/Common/CollectionView/AutosizedCollectionView.swift b/Riot/Modules/Common/CollectionView/AutosizedCollectionView.swift new file mode 100644 index 000000000..10cdbf75c --- /dev/null +++ b/Riot/Modules/Common/CollectionView/AutosizedCollectionView.swift @@ -0,0 +1,31 @@ +/* + 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 + +/// AutosizedCollectionView is a convenient UICollectionView that makes dynamic sizing easier when using Auto Layout +class AutosizedCollectionView: UICollectionView { + + override var contentSize: CGSize { + didSet { + self.invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + return self.contentSize + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift new file mode 100644 index 000000000..3b3d46a03 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift @@ -0,0 +1,107 @@ +/* + 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 BubbleReactionViewCell: UICollectionViewCell, NibReusable, Themable { + + // MARK: - Constants + + private enum Constants { + static let selectedBorderWidth: CGFloat = 1.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var reactionBackgroundView: UIView! + @IBOutlet private weak var emojiLabel: UILabel! + @IBOutlet private weak var countLabel: UILabel! + + // MARK: Private + + private var theme: Theme? + + // MARK: Public + + private var isReactionSelected: Bool = false + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + + self.reactionBackgroundView.layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.reactionBackgroundView.layer.cornerRadius = self.reactionBackgroundView.frame.size.height/2.0 + } + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + if #available(iOS 12.0, *) { + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + } + return super.preferredLayoutAttributesFitting(layoutAttributes) + } + + // MARK: - Public + + func fill(viewData: BubbleReactionViewData) { + self.emojiLabel.text = viewData.emoji + self.countLabel.text = viewData.countString + self.isReactionSelected = viewData.isCurrentUserReacted + + self.updateViews() + } + + func update(theme: Theme) { + self.theme = theme + self.reactionBackgroundView.layer.borderColor = self.theme?.tintColor.cgColor + self.countLabel.textColor = self.theme?.textPrimaryColor + self.updateViews() + } + + // MARK: - Private + + private func updateViews() { + + let reactionBackgroundColor: UIColor? + let reactionBackgroundBorderWidth: CGFloat + + if self.isReactionSelected { + reactionBackgroundColor = self.theme?.tintBackgroundColor + reactionBackgroundBorderWidth = Constants.selectedBorderWidth + } else { + reactionBackgroundColor = self.theme?.headerBackgroundColor + reactionBackgroundBorderWidth = 0.0 + } + + self.reactionBackgroundView.layer.borderWidth = reactionBackgroundBorderWidth + self.reactionBackgroundView.backgroundColor = reactionBackgroundColor + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.xib b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.xib new file mode 100644 index 000000000..35e41eec8 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.xib @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewData.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewData.swift new file mode 100644 index 000000000..316f9c34b --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewData.swift @@ -0,0 +1,23 @@ +/* + 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 + +struct BubbleReactionViewData { + let emoji: String + let countString: String + let isCurrentUserReacted: Bool +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift new file mode 100644 index 000000000..33eaf486c --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift @@ -0,0 +1,138 @@ +/* + 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 +import MatrixSDK +import Reusable +import DGCollectionViewLeftAlignFlowLayout + +@objcMembers +final class BubbleReactionsView: UIView, NibOwnerLoadable { + + // MARK: - Constants + + private enum Constants { + static let minimumInteritemSpacing: CGFloat = 6.0 + static let minimumLineSpacing: CGFloat = 2.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var collectionView: UICollectionView! + + // MARK: Private + + private var reactionsViewData: [BubbleReactionViewData] = [] + private var theme: Theme? + + // MARK: Public + + // Do not use `BubbleReactionsViewModelType` here due to Objective-C incompatibily + var viewModel: BubbleReactionsViewModel? { + didSet { + self.viewModel?.viewDelegate = self + self.viewModel?.process(viewAction: .loadData) + } + } + + // MARK: - Setup + + private func commonInit() { + self.collectionView.isScrollEnabled = false + self.collectionView.delegate = self + self.collectionView.dataSource = self + self.collectionView.collectionViewLayout = DGCollectionViewLeftAlignFlowLayout() + + if let collectionViewFlowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout { + collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + collectionViewFlowLayout.minimumInteritemSpacing = Constants.minimumInteritemSpacing + collectionViewFlowLayout.minimumLineSpacing = Constants.minimumLineSpacing + } + + self.collectionView.register(cellType: BubbleReactionViewCell.self) + self.collectionView.reloadData() + } + + 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 update(theme: Theme) { + self.theme = theme + self.collectionView.reloadData() + } + + func fill(reactionsViewData: [BubbleReactionViewData]) { + self.reactionsViewData = reactionsViewData + self.collectionView.reloadData() + } +} + +// MARK: - UICollectionViewDataSource +extension BubbleReactionsView: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.reactionsViewData.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: BubbleReactionViewCell = collectionView.dequeueReusableCell(for: indexPath) + + if let theme = self.theme { + cell.update(theme: theme) + } + + let viewData = self.reactionsViewData[indexPath.row] + cell.fill(viewData: viewData) + + return cell + } +} + +// MARK: - UICollectionViewDelegate +extension BubbleReactionsView: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + self.viewModel?.process(viewAction: .tapReaction(index: indexPath.row)) + } +} + +// MARK: - BubbleReactionsViewModelViewDelegate +extension BubbleReactionsView: BubbleReactionsViewModelViewDelegate { + + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didUpdateViewState viewState: BubbleReactionsViewState) { + switch viewState { + case .loaded(reactionsViewData: let reactionsViewData): + self.fill(reactionsViewData: reactionsViewData) + } + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.xib b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.xib new file mode 100644 index 000000000..7c5cedb2a --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModel.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModel.swift new file mode 100644 index 000000000..339b00987 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModel.swift @@ -0,0 +1,66 @@ +/* + 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 final class BubbleReactionsViewModel: NSObject, BubbleReactionsViewModelType { + + // MARK: - Properties + + // MARK: Private + + private let aggregatedReactions: MXAggregatedReactions + private let reactionsViewData: [BubbleReactionViewData] + private let eventId: String + + // MARK: Public + + @objc weak var viewModelDelegate: BubbleReactionsViewModelDelegate? + weak var viewDelegate: BubbleReactionsViewModelViewDelegate? + + // MARK: - Setup + + @objc init(aggregatedReactions: MXAggregatedReactions, + eventId: String) { + self.aggregatedReactions = aggregatedReactions + self.eventId = eventId + + self.reactionsViewData = aggregatedReactions.reactions.map { (reactionCount) -> BubbleReactionViewData in + return BubbleReactionViewData(emoji: reactionCount.reaction, countString: "\(reactionCount.count)", isCurrentUserReacted: reactionCount.myUserHasReacted) + } + } + + // MARK: - Public + + func process(viewAction: BubbleReactionsViewAction) { + switch viewAction { + case .loadData: + self.viewDelegate?.bubbleReactionsViewModel(self, didUpdateViewState: .loaded(reactionsViewData: self.reactionsViewData)) + case .tapReaction(let index): + guard index < self.aggregatedReactions.reactions.count else { + return + } + let reactionCount = self.aggregatedReactions.reactions[index] + if reactionCount.myUserHasReacted { + self.viewModelDelegate?.bubbleReactionsViewModel(self, didRemoveReaction: reactionCount, forEventId: self.eventId) + } else { + self.viewModelDelegate?.bubbleReactionsViewModel(self, didAddReaction: reactionCount, forEventId: self.eventId) + } + case .addNewReaction: + break + } + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModelType.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModelType.swift new file mode 100644 index 000000000..04a30726a --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModelType.swift @@ -0,0 +1,43 @@ +/* + 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 + +enum BubbleReactionsViewAction { + case loadData + case tapReaction(index: Int) + case addNewReaction +} + +enum BubbleReactionsViewState { + case loaded(reactionsViewData: [BubbleReactionViewData]) +} + +@objc protocol BubbleReactionsViewModelDelegate: class { + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didAddReaction reactionCount: MXReactionCount, forEventId eventId: String) + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didRemoveReaction reactionCount: MXReactionCount, forEventId eventId: String) +} + +protocol BubbleReactionsViewModelViewDelegate: class { + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didUpdateViewState viewState: BubbleReactionsViewState) +} + +protocol BubbleReactionsViewModelType { + var viewModelDelegate: BubbleReactionsViewModelDelegate? { get set } + var viewDelegate: BubbleReactionsViewModelViewDelegate? { get set } + + func process(viewAction: BubbleReactionsViewAction) +} diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index df44185b8..bd7aef55a 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -22,6 +22,8 @@ #import "AvatarGenerator.h" #import "Tools.h" +#import "Riot-Swift.h" + static NSAttributedString *timestampVerticalWhitespace = nil; static NSAttributedString *readReceiptVerticalWhitespace = nil; @@ -357,11 +359,28 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; { // Add vertical whitespace in case of read receipts. NSUInteger reactionCount = self.reactions[eventId].reactions.count; + + MXAggregatedReactions *aggregatedReactions = self.reactions[eventId]; + if (reactionCount) { - // TODO: Set right height: 22 + 8 - // TODO: Set right dynamic line count: reactionCount / 6 - CGFloat height = (22 + 8) * ((reactionCount / 6) + 1); + CGSize fittingSize = UILayoutFittingCompressedSize; + fittingSize.width = self.maxTextViewWidth; + + static BubbleReactionsView *bubbleReactionsView; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + bubbleReactionsView = [BubbleReactionsView new]; + }); + + bubbleReactionsView.frame = CGRectMake(0, 0, self.maxTextViewWidth, 1.0); + BubbleReactionsViewModel *viemModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId]; + bubbleReactionsView.viewModel = viemModel; + [bubbleReactionsView layoutIfNeeded]; + + CGFloat height = [bubbleReactionsView systemLayoutSizeFittingSize:fittingSize].height; + [attributedString appendAttributedString:[RoomBubbleCellData verticalWhitespaceForHeight: height]]; } @@ -503,7 +522,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; return [[NSAttributedString alloc] initWithString:returnString attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], - NSFontAttributeName: [UIFont systemFontOfSize:4]}]; + NSFontAttributeName: [UIFont systemFontOfSize:6]}]; } - (BOOL)hasSameSenderAsBubbleCellData:(id)bubbleCellData diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 02858a7aa..29cbe5cd8 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -27,7 +27,11 @@ #import "MXRoom+Riot.h" -@interface RoomDataSource() + +static CGFloat kBubbleReactionsViewLeftMargin = 55.0; +static CGFloat kBubbleReactionsViewRightMargin = 15.0; + +@interface RoomDataSource() { // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; @@ -275,6 +279,7 @@ while (index--) { MXKRoomBubbleComponent *component = bubbleComponents[index]; + NSString *componentEventId = component.event.eventId; if (component.event.sentState != MXEventSentStateFailed) { @@ -379,54 +384,46 @@ [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, topConstraint, trailingConstraint]]; } } - - MXAggregatedReactions* reactions = cellData.reactions[component.event.eventId]; + + MXAggregatedReactions* reactions = cellData.reactions[componentEventId]; + if (reactions && !isCollapsableCellCollapsed) { - // TODO: Use final ReactionsView - UITextView* reactionsContainer = [UITextView new]; - reactionsContainer.translatesAutoresizingMaskIntoConstraints = NO; - [bubbleCell.contentView addSubview:reactionsContainer]; - - reactionsContainer.layer.borderColor = UIColor.orangeColor.CGColor; - reactionsContainer.layer.borderWidth = 1; - + BubbleReactionsViewModel *bubbleReactionsViewModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:reactions eventId:componentEventId]; + + BubbleReactionsView *reactionsView = [BubbleReactionsView new]; + reactionsView.viewModel = bubbleReactionsViewModel; + [reactionsView updateWithTheme:ThemeService.shared.theme]; + + bubbleReactionsViewModel.viewModelDelegate = self; + + reactionsView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:reactionsView]; + if (!bubbleCell.tmpSubviews) { bubbleCell.tmpSubviews = [NSMutableArray array]; } - [bubbleCell.tmpSubviews addObject:reactionsContainer]; - + [bubbleCell.tmpSubviews addObject:reactionsView]; + // At the bottom, we have read receipts or nothing NSLayoutConstraint *bottomConstraint; if (avatarsContainer) { - bottomConstraint = [reactionsContainer.bottomAnchor constraintEqualToAnchor:avatarsContainer.topAnchor]; + bottomConstraint = [reactionsView.bottomAnchor constraintEqualToAnchor:avatarsContainer.topAnchor]; } else { - bottomConstraint = [reactionsContainer.bottomAnchor constraintEqualToAnchor:reactionsContainer.superview.topAnchor constant:bottomPositionY]; + bottomConstraint = [reactionsView.bottomAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY]; } - - // TODO: To refine - CGFloat viewHeight = 22; - + // Force receipts container size [NSLayoutConstraint activateConstraints: - @[ - [reactionsContainer.leadingAnchor constraintEqualToAnchor:reactionsContainer.superview.leadingAnchor constant:50], - [reactionsContainer.trailingAnchor constraintEqualToAnchor:reactionsContainer.superview.trailingAnchor constant:-6], - [reactionsContainer.heightAnchor constraintEqualToConstant:viewHeight], - bottomConstraint - ]]; - - // TODO: To remove - NSMutableString *reactionsString = [NSMutableString string]; - for (MXReactionCount *reactionCount in reactions.reactions) - { - [reactionsString appendFormat:@"%@: %@ ", reactionCount.reaction, @(reactionCount.count)]; - } - reactionsContainer.text = reactionsString; + @[ + [reactionsView.leadingAnchor constraintEqualToAnchor:reactionsView.superview.leadingAnchor constant:kBubbleReactionsViewLeftMargin], + [reactionsView.trailingAnchor constraintEqualToAnchor:reactionsView.superview.trailingAnchor constant:-kBubbleReactionsViewRightMargin], + bottomConstraint + ]]; } // Check whether the read marker must be displayed here. @@ -439,7 +436,7 @@ bubbleCell.bubbleOverlayContainer.userInteractionEnabled = NO; bubbleCell.bubbleOverlayContainer.hidden = NO; - if ([component.event.eventId isEqualToString:self.room.accountData.readMarkerEventId]) + if ([componentEventId isEqualToString:self.room.accountData.readMarkerEventId]) { bubbleCell.readMarkerView = [[UIView alloc] initWithFrame:CGRectMake(0, bottomPositionY - 2, bubbleCell.bubbleOverlayContainer.frame.size.width, 2)]; bubbleCell.readMarkerView.backgroundColor = ThemeService.shared.theme.tintColor; @@ -580,4 +577,24 @@ return jitsiWidget; } +#pragma mark - BubbleReactionsViewModelDelegate + +- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didAddReaction:(MXReactionCount *)reactionCount forEventId:(NSString *)eventId +{ + [self.mxSession.aggregations sendReaction:reactionCount.reaction toEvent:eventId inRoom:self.roomId success:^(NSString * _Nonnull eventId) { + + } failure:^(NSError * _Nonnull error) { + NSLog(@"[MXKRoomDataSource] Fail to send reaction on eventId: %@", eventId); + }]; +} + +- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didRemoveReaction:(MXReactionCount * _Nonnull)reactionCount forEventId:(NSString * _Nonnull)eventId +{ + [self.mxSession.aggregations unReactOnReaction:reactionCount.reaction toEvent:eventId inRoom:self.roomId success:^{ + + } failure:^(NSError * _Nonnull error) { + NSLog(@"[MXKRoomDataSource] Fail to unreact on eventId: %@", eventId); + }]; +} + @end