From efa726b515d98a8bc7128bc1bc761f8ee1a559c5 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 26 Oct 2021 16:27:51 +0300 Subject: [PATCH] vector-im/element-ios/issues/4976 - Replaced GrowingTextView with simpler, custom implementation. Cleaned up the RoomInputToolbar header. --- Podfile | 1 - .../RoomInputToolbarTextView.swift | 115 ++++++++++++++++-- .../Views/InputToolbar/RoomInputToolbarView.h | 44 +++---- .../Views/InputToolbar/RoomInputToolbarView.m | 98 +++++++-------- changelog.d/4976.change | 1 + 5 files changed, 172 insertions(+), 87 deletions(-) create mode 100644 changelog.d/4976.change diff --git a/Podfile b/Podfile index dec1aaefb..f2fab251b 100644 --- a/Podfile +++ b/Podfile @@ -73,7 +73,6 @@ abstract_target 'RiotPods' do pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' pod 'ffmpeg-kit-ios-audio', '~> 4.5' - pod 'GrowingTextView', '~> 0.7.2' pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift index 729b3c2e7..4e8916e54 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift @@ -14,26 +14,91 @@ // limitations under the License. // -import GrowingTextView - @objc protocol RoomInputToolbarTextViewDelegate: AnyObject { + func textView(_ textView: RoomInputToolbarTextView, didChangeHeight height: CGFloat) func textView(_ textView: RoomInputToolbarTextView, didReceivePasteForMediaFromSender sender: Any?) } -class RoomInputToolbarTextView: GrowingTextView { +@objcMembers +class RoomInputToolbarTextView: UITextView { - @objc weak var toolbarDelegate: RoomInputToolbarTextViewDelegate? + private var heightConstraint: NSLayoutConstraint! + + weak var toolbarDelegate: RoomInputToolbarTextViewDelegate? + + var placeholder: String? + var placeholderColor: UIColor = UIColor(white: 0.8, alpha: 1.0) - override var keyCommands: [UIKeyCommand]? { - return [UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(keyCommandSelector(_:)))] + var minHeight: CGFloat = 30.0 { + didSet { + updateUI() + } } - @objc private func keyCommandSelector(_ keyCommand: UIKeyCommand) { - guard keyCommand.input == "\r", let delegate = (self.delegate as? RoomInputToolbarView) else { + var maxHeight: CGFloat = 0.0 { + didSet { + updateUI() + } + } + + override var text: String! { + didSet { + updateUI() + } + } + + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + private func commonInit() { + contentMode = .redraw + + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), name: UITextView.textDidChangeNotification, object: self) + + if let heightConstraint = constraints.filter({ $0.firstAttribute == .height && $0.relation == .equal }).first { + self.heightConstraint = heightConstraint + } else { + heightConstraint = self.heightAnchor.constraint(equalToConstant: minHeight) + addConstraint(heightConstraint) + } + } + + // MARK: - Overrides + + override func layoutSubviews() { + super.layoutSubviews() + updateUI() + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + guard text.isEmpty, let placeholder = placeholder else { return } - delegate.onTouchUp(inside: delegate.rightInputToolbarButton) + var attributes: [NSAttributedString.Key: Any] = [.foregroundColor: placeholderColor] + if let font = font { + attributes[.font] = font + } + + let frame = rect.inset(by: .init(top: textContainerInset.top, + left: textContainerInset.left + textContainer.lineFragmentPadding, + bottom: textContainerInset.bottom, + right: textContainerInset.right)) + + placeholder.draw(in: frame, withAttributes: attributes) + } + + override var keyCommands: [UIKeyCommand]? { + return [UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(keyCommandSelector(_:)))] } /// Overrides paste to handle images pasted from Safari, passing them up to the input toolbar. @@ -49,4 +114,36 @@ class RoomInputToolbarTextView: GrowingTextView { super.paste(sender) } } + + // MARK: - Private + + @objc private func textDidChange(notification: Notification) { + if let sender = notification.object as? RoomInputToolbarTextView, sender == self { + updateUI() + } + } + + private func updateUI() { + var height = sizeThatFits(CGSize(width: bounds.size.width, height: CGFloat.greatestFiniteMagnitude)).height + height = minHeight > 0 ? max(height, minHeight) : height + height = maxHeight > 0 ? min(height, maxHeight) : height + + // Update placeholder + self.setNeedsDisplay() + + guard height != heightConstraint.constant else { + return + } + + heightConstraint.constant = height + toolbarDelegate?.textView(self, didChangeHeight: height) + } + + @objc private func keyCommandSelector(_ keyCommand: UIKeyCommand) { + guard keyCommand.input == "\r", let delegate = (self.delegate as? RoomInputToolbarView) else { + return + } + + delegate.onTouchUp(inside: delegate.rightInputToolbarButton) + } } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 3099fd50f..d126ba22f 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -60,28 +60,10 @@ typedef enum : NSUInteger */ @property (nonatomic, weak) id delegate; -@property (weak, nonatomic) IBOutlet UIView *mainToolbarView; - -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainToolbarMinHeightConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainToolbarHeightConstraint; - -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageComposerContainerTrailingConstraint; - -@property (weak, nonatomic) IBOutlet UIButton *attachMediaButton; - -@property (weak, nonatomic) IBOutlet UIImageView *inputTextBackgroundView; - -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *inputContextViewHeightConstraint; -@property (weak, nonatomic) IBOutlet UIImageView *inputContextImageView; -@property (weak, nonatomic) IBOutlet UILabel *inputContextLabel; -@property (weak, nonatomic) IBOutlet UIButton *inputContextButton; -@property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar; -@property (weak, nonatomic) UIView *voiceMessageToolbarView; - /** Tell whether the filled data will be sent encrypted. NO by default. */ -@property (nonatomic) BOOL isEncryptionEnabled; +@property (nonatomic, assign) BOOL isEncryptionEnabled; /** Sender of the event being edited / replied. @@ -91,11 +73,31 @@ typedef enum : NSUInteger /** Destination of the message in the composer. */ -@property (nonatomic) RoomInputToolbarViewSendMode sendMode; +@property (nonatomic, assign) RoomInputToolbarViewSendMode sendMode; /** YES if action menu is opened. NO otherwise */ -@property (nonatomic, getter=isActionMenuOpened) BOOL actionMenuOpened; +@property (nonatomic, assign) BOOL actionMenuOpened; + +/** + The input toolbar's main height constraint + */ +@property (nonatomic, weak, readonly) NSLayoutConstraint *mainToolbarHeightConstraint; + +/** + The input toolbar's action bar + */ +@property (nonatomic, weak, readonly) RoomActionsBar *actionsBar; + +/** + The attach media button + */ +@property (nonatomic, weak, readonly) UIButton *attachMediaButton; + +/** + Adds a voice message toolbar view to be displayed inside this input toolbar + */ +- (void)setVoiceMessageToolbarView:(UIView *)toolbarView; @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 06c509632..bf1bda55c 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -17,34 +17,40 @@ #import "RoomInputToolbarView.h" -#import "ThemeService.h" #import "Riot-Swift.h" - #import "GBDeviceInfo_iOS.h" -#import "UINavigationController+Riot.h" +static const CGFloat kContextBarHeight = 24; +static const CGFloat kActionMenuAttachButtonSpringVelocity = 7; +static const CGFloat kActionMenuAttachButtonSpringDamping = .45; -#import "WidgetManager.h" -#import "IntegrationManagerViewController.h" +static const NSTimeInterval kSendModeAnimationDuration = .15; +static const NSTimeInterval kActionMenuAttachButtonAnimationDuration = .4; +static const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; +static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; -@import GrowingTextView; +@interface RoomInputToolbarView() -const double kContextBarHeight = 24; -const NSTimeInterval kSendModeAnimationDuration = .15; -const NSTimeInterval kActionMenuAttachButtonAnimationDuration = .4; -const CGFloat kActionMenuAttachButtonSpringVelocity = 7; -const CGFloat kActionMenuAttachButtonSpringDamping = .45; -const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; -const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; -const CGFloat kComposerContainerTrailingPadding = 12; +@property (nonatomic, weak) IBOutlet UIView *mainToolbarView; -@interface RoomInputToolbarView() -{ - // The intermediate action sheet - UIAlertController *actionSheet; -} +@property (nonatomic, weak) IBOutlet UIButton *attachMediaButton; @property (nonatomic, weak) IBOutlet RoomInputToolbarTextView *textView; +@property (nonatomic, weak) IBOutlet UIImageView *inputTextBackgroundView; + +@property (nonatomic, weak) IBOutlet UIImageView *inputContextImageView; +@property (nonatomic, weak) IBOutlet UILabel *inputContextLabel; +@property (nonatomic, weak) IBOutlet UIButton *inputContextButton; + +@property (nonatomic, weak) IBOutlet RoomActionsBar *actionsBar; + +@property (nonatomic, weak) IBOutlet NSLayoutConstraint *mainToolbarMinHeightConstraint; +@property (nonatomic, weak) IBOutlet NSLayoutConstraint *mainToolbarHeightConstraint; +@property (nonatomic, weak) IBOutlet NSLayoutConstraint *messageComposerContainerTrailingConstraint; +@property (nonatomic, weak) IBOutlet NSLayoutConstraint *inputContextViewHeightConstraint; + +@property (nonatomic, weak) UIView *voiceMessageToolbarView; + @property (nonatomic, assign) CGFloat expandedMainToolbarHeight; @end @@ -52,22 +58,10 @@ const CGFloat kComposerContainerTrailingPadding = 12; @implementation RoomInputToolbarView @dynamic delegate; -+ (UINib *)nib -{ - return [UINib nibWithNibName:NSStringFromClass([RoomInputToolbarView class]) - bundle:[NSBundle bundleForClass:[RoomInputToolbarView class]]]; -} - + (instancetype)roomInputToolbarView { - if ([[self class] nib]) - { - return [[[self class] nib] instantiateWithOwner:nil options:nil].firstObject; - } - else - { - return [[self alloc] init]; - } + UINib *nib = [UINib nibWithNibName:NSStringFromClass([RoomInputToolbarView class]) bundle:nil]; + return [nib instantiateWithOwner:nil options:nil].firstObject; } - (void)awakeFromNib @@ -315,6 +309,11 @@ const CGFloat kComposerContainerTrailingPadding = 12; self.textView.placeholder = inPlaceholder; } +- (void)pasteText:(NSString *)text +{ + self.textMessage = [self.textView.text stringByReplacingCharactersInRange:self.textView.selectedRange withString:text]; +} + #pragma mark - Actions - (IBAction)cancelAction:(id)sender @@ -325,7 +324,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; } } -#pragma mark - GrowingTextViewDelegate +#pragma mark - UITextViewDelegate - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { @@ -351,7 +350,9 @@ const CGFloat kComposerContainerTrailingPadding = 12; [self.delegate roomInputToolbarViewDidChangeTextMessage:self]; } -- (void)textViewDidChangeHeight:(GrowingTextView *)textView height:(CGFloat)height +#pragma mark - RoomInputToolbarTextViewDelegate + +- (void)textView:(RoomInputToolbarTextView *)textView didChangeHeight:(CGFloat)height { // Update height of the main toolbar (message composer) CGFloat updatedHeight = height + (self.messageComposerContainerTopConstraint.constant + self.messageComposerContainerBottomConstraint.constant) + self.inputContextViewHeightConstraint.constant; @@ -376,13 +377,18 @@ const CGFloat kComposerContainerTrailingPadding = 12; } } +- (void)textView:(RoomInputToolbarTextView *)textView didReceivePasteForMediaFromSender:(id)sender +{ + [self paste:sender]; +} + #pragma mark - Override MXKRoomInputToolbarView - (IBAction)onTouchUpInside:(UIButton*)button { if (button == self.attachMediaButton) { - self.actionMenuOpened = !self.isActionMenuOpened; + self.actionMenuOpened = !self.actionMenuOpened; } [super onTouchUpInside:button]; @@ -400,12 +406,6 @@ const CGFloat kComposerContainerTrailingPadding = 12; - (void)destroy { - if (actionSheet) - { - [actionSheet dismissViewControllerAnimated:NO completion:nil]; - actionSheet = nil; - } - [super destroy]; } @@ -462,20 +462,6 @@ const CGFloat kComposerContainerTrailingPadding = 12; } } -#pragma mark - Clipboard - Handle image/data paste from general pasteboard - -- (void)paste:(id)sender -{ - // TODO Custom here the validation screen for each available item - - [super paste:sender]; -} - -- (void)textView:(GrowingTextView *)textView didReceivePasteForMediaFromSender:(id)sender -{ - [self paste:sender]; -} - #pragma mark - Private - (void)updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated diff --git a/changelog.d/4976.change b/changelog.d/4976.change new file mode 100644 index 000000000..7c52865d3 --- /dev/null +++ b/changelog.d/4976.change @@ -0,0 +1 @@ +Replaced GrowingTextView with simpler, custom implementation. Cleaned up the RoomInputToolbar header. \ No newline at end of file