Switching composer between text mode & action mode

This commit is contained in:
Gil Eluard
2021-03-25 22:15:18 +01:00
parent 265d6bc5d6
commit 8c21eb0ff6
25 changed files with 467 additions and 210 deletions
@@ -0,0 +1,30 @@
//
// Copyright 2021 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
@objcMembers
@objc class RoomActionItem: NSObject {
var image: UIImage!
var action: (() -> Void)!
init(image: UIImage, andAction action: @escaping () -> Void) {
super.init()
self.image = image
self.action = action
}
}
@@ -0,0 +1,129 @@
//
// Copyright 2021 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
@objcMembers
@objc class RoomActionsBar: UIScrollView {
// MARK: - Properties
var itemSpacing: CGFloat = 20 {
didSet {
self.setNeedsLayout()
}
}
var actionItems: Array<RoomActionItem> = [] {
didSet {
var actionButtons: Array<UIButton> = []
for (index, item) in actionItems.enumerated() {
let button = UIButton(type: .custom)
button.setImage(item.image, for: .normal)
button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
button.tintColor = ThemeService.shared().theme.tintColor
button.tag = index
actionButtons.append(button)
addSubview(button)
}
self.actionButtons = actionButtons
self.lastBounds = .zero
self.setNeedsLayout()
}
}
private var actionButtons: Array<UIButton> = [] {
willSet {
for button in actionButtons {
button.removeFromSuperview()
}
}
}
private var lastBounds = CGRect.zero
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
self.showsHorizontalScrollIndicator = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.showsHorizontalScrollIndicator = false
}
override func layoutSubviews() {
super.layoutSubviews()
guard lastBounds != self.bounds else {
return
}
lastBounds = self.bounds
var currentX: CGFloat = 0
for button in actionButtons {
button.frame = CGRect(x: currentX, y: 0, width: self.bounds.height, height: self.bounds.height)
currentX = button.frame.maxX + itemSpacing
}
self.contentSize = CGSize(width: currentX - itemSpacing, height: self.bounds.height)
}
// MARK: - Business methods
func customizeViewRendering() {
for button in actionButtons {
button.tintColor = ThemeService.shared().theme.tintColor
}
}
func animate(showIn: Bool, completion: ((Bool) -> Void)? = nil) {
if showIn {
for button in actionButtons {
button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height)
}
for (index, button) in actionButtons.enumerated() {
UIView.animate(withDuration: 0.32, delay: 0.05 * Double(index), usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut) {
button.transform = CGAffineTransform.identity
} completion: { (finished) in
completion?(finished)
}
}
} else {
for (index, button) in actionButtons.enumerated() {
UIView.animate(withDuration: 0.2, delay: 0.05 * Double(index), options: .curveEaseInOut) {
button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height)
} completion: { (finished) in
completion?(finished)
}
}
}
}
// MARK: - Private methods
@objc private func buttonAction(_ sender: UIButton) {
if let action = actionItems[sender.tag].action {
action()
}
}
private func setupView() {
self.showsHorizontalScrollIndicator = false
}
}
@@ -18,6 +18,8 @@
#import "MediaPickerViewController.h"
@class RoomActionsBar;
/**
Destination of the message in the composer
*/
@@ -31,34 +33,6 @@ typedef enum : NSUInteger
@protocol RoomInputToolbarViewDelegate <MXKRoomInputToolbarViewDelegate>
/**
Tells the delegate that the user wants to display the sticker picker.
@param toolbarView the room input toolbar view.
*/
- (void)roomInputToolbarViewPresentStickerPicker:(MXKRoomInputToolbarView*)toolbarView;
/**
Tells the delegate that the user wants to send external files.
@param toolbarView the room input toolbar view
*/
- (void)roomInputToolbarViewDidTapFileUpload:(MXKRoomInputToolbarView*)toolbarView;
/**
Tells the delegate that the user wants to take photo or video with camera.
@param toolbarView the room input toolbar view
*/
- (void)roomInputToolbarViewDidTapCamera:(MXKRoomInputToolbarView*)toolbarView;
/**
Tells the delegate that the user wants to show media library.
@param toolbarView the room input toolbar view
*/
- (void)roomInputToolbarViewDidTapMediaLibrary:(MXKRoomInputToolbarView*)toolbarView;
/**
Tells the delegate that the user wants to cancel the current edition / reply.
@@ -95,6 +69,7 @@ typedef enum : NSUInteger
@property (weak, nonatomic) IBOutlet UIImageView *inputContextImageView;
@property (weak, nonatomic) IBOutlet UILabel *inputContextLabel;
@property (weak, nonatomic) IBOutlet UIButton *inputContextButton;
@property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar;
/**
Tell whether the filled data will be sent encrypted. NO by default.
@@ -111,4 +86,9 @@ typedef enum : NSUInteger
*/
@property (nonatomic) RoomInputToolbarViewSendMode sendMode;
/**
YES if action menu is opened. NO otherwise
*/
@property (nonatomic, getter=isActionMenuOpened) BOOL actionMenuOpened;
@end
@@ -120,6 +120,7 @@ const double RoomInputToolbarViewContextBarHeight = 30;
self.inputContextImageView.tintColor = ThemeService.shared.theme.textSecondaryColor;
self.inputContextLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor;
[self.actionsBar customizeViewRendering];
}
#pragma mark -
@@ -142,6 +143,7 @@ const double RoomInputToolbarViewContextBarHeight = 30;
RoomInputToolbarViewSendMode previousMode = _sendMode;
_sendMode = sendMode;
self.actionMenuOpened = NO;
[self updatePlaceholder];
[self updateToolbarButtonLabelWithPreviousMode: previousMode];
}
@@ -329,92 +331,7 @@ const double RoomInputToolbarViewContextBarHeight = 30;
{
if (button == self.attachMediaButton)
{
// Check whether media attachment is supported
if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:presentViewController:)])
{
// Ask the user the kind of the call: voice or video?
actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
__weak typeof(self) weakSelf = self;
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_camera", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->actionSheet = nil;
[self.delegate roomInputToolbarViewDidTapCamera:self];
}
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_photo_or_video", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->actionSheet = nil;
[self.delegate roomInputToolbarViewDidTapMediaLibrary:self];
}
}]];
if (BuildSettings.allowSendingStickers)
{
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_sticker", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->actionSheet = nil;
[self.delegate roomInputToolbarViewPresentStickerPicker:self];
}
}]];
}
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_file", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->actionSheet = nil;
[self.delegate roomInputToolbarViewDidTapFileUpload:self];
}
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->actionSheet = nil;
}
}]];
[actionSheet popoverPresentationController].sourceView = self.attachMediaButton;
[actionSheet popoverPresentationController].sourceRect = self.attachMediaButton.bounds;
[self.window.rootViewController presentViewController:actionSheet animated:YES completion:nil];
}
else
{
NSLog(@"[RoomInputToolbarView] Attach media is not supported");
}
self.actionMenuOpened = !self.isActionMenuOpened;
}
[super onTouchUpInside:button];
@@ -443,9 +360,55 @@ const double RoomInputToolbarViewContextBarHeight = 30;
self.rightInputToolbarButton.alpha = 0;
self.messageComposerContainerTrailingConstraint.constant = 12;
}
[self layoutIfNeeded];
}
#pragma mark - properties
- (void)setActionMenuOpened:(BOOL)actionMenuOpened
{
if (_actionMenuOpened != actionMenuOpened)
{
_actionMenuOpened = actionMenuOpened;
if (_actionMenuOpened) {
self.actionsBar.hidden = NO;
[self.actionsBar animateWithShowIn:_actionMenuOpened completion:nil];
}
else
{
[self.actionsBar animateWithShowIn:_actionMenuOpened completion:^(BOOL finished) {
self.actionsBar.hidden = YES;
}];
}
[UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:0.45 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.attachMediaButton.transform = actionMenuOpened ? CGAffineTransformMakeRotation(M_PI * 3 / 4) : CGAffineTransformIdentity;
} completion:^(BOOL finished) {
}];
[UIView animateWithDuration:.2 delay:_actionMenuOpened ? 0 : .1 options:UIViewAnimationOptionCurveEaseIn animations:^{
self->messageComposerContainer.alpha = actionMenuOpened ? 0 : 1;
self.rightInputToolbarButton.alpha = self->growingTextView.text.length == 0 || actionMenuOpened ? 0 : 1;
} completion:^(BOOL finished) {
}];
[UIView animateWithDuration:.3 animations:^{
if (actionMenuOpened)
{
self.mainToolbarHeightConstraint.constant = self.mainToolbarMinHeightConstraint.constant;
}
else
{
[self->growingTextView refreshHeight];
}
[self layoutIfNeeded];
[self.delegate roomInputToolbarView:self heightDidChanged:self.mainToolbarHeightConstraint.constant completion:nil];
}];
}
}
#pragma mark - Clipboard - Handle image/data paste from general pasteboard
- (void)paste:(id)sender
@@ -27,6 +27,14 @@
<action selector="onTouchUpInside:" destination="iN0-l3-epB" eventType="touchUpInside" id="WbU-WH-gwL"/>
</connections>
</button>
<scrollView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ESv-9w-KJF" customClass="RoomActionsBar" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="60" y="8" width="540" height="38"/>
<constraints>
<constraint firstAttribute="height" constant="38" id="i6C-gL-ADZ"/>
</constraints>
<viewLayoutGuide key="contentLayoutGuide" id="F6O-76-cZl"/>
<viewLayoutGuide key="frameLayoutGuide" id="rZR-Bv-AqG"/>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QWp-NV-uh5" userLabel="Message Composer Container">
<rect key="frame" x="60" y="9" width="528" height="36"/>
<subviews>
@@ -105,11 +113,14 @@
<constraint firstItem="QWp-NV-uh5" firstAttribute="leading" secondItem="Hga-l8-Wua" secondAttribute="trailing" constant="12" id="M9f-je-3zO"/>
<constraint firstAttribute="bottom" secondItem="QWp-NV-uh5" secondAttribute="bottom" constant="13" id="NGr-2o-sOP"/>
<constraint firstAttribute="trailing" secondItem="G8Z-CM-tGs" secondAttribute="trailing" constant="12" id="Sua-LC-3yW"/>
<constraint firstItem="ESv-9w-KJF" firstAttribute="leading" secondItem="Hga-l8-Wua" secondAttribute="trailing" constant="12" id="TIe-py-lFJ"/>
<constraint firstItem="QWp-NV-uh5" firstAttribute="top" secondItem="a84-Vc-6ud" secondAttribute="top" constant="9" id="WyZ-3i-OHi"/>
<constraint firstAttribute="bottom" secondItem="G8Z-CM-tGs" secondAttribute="bottom" constant="12" id="Yam-dS-zwr"/>
<constraint firstAttribute="height" constant="58" id="Yjj-ua-rbe"/>
<constraint firstAttribute="bottom" secondItem="Hga-l8-Wua" secondAttribute="bottom" constant="12" id="b0G-CY-AmP"/>
<constraint firstAttribute="trailing" secondItem="QWp-NV-uh5" secondAttribute="trailing" constant="12" id="hXO-cY-Jgz"/>
<constraint firstAttribute="trailing" secondItem="ESv-9w-KJF" secondAttribute="trailing" id="jCS-Tf-vxr"/>
<constraint firstAttribute="bottom" secondItem="ESv-9w-KJF" secondAttribute="bottom" constant="12" id="v8r-ac-MKn"/>
</constraints>
</view>
</subviews>
@@ -125,6 +136,7 @@
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="actionsBar" destination="ESv-9w-KJF" id="h7H-vz-yzO"/>
<outlet property="attachMediaButton" destination="Hga-l8-Wua" id="Osr-ek-c91"/>
<outlet property="growingTextView" destination="wgb-ON-N29" id="nwF-uV-Ng9"/>
<outlet property="inputContextButton" destination="48y-kn-7b5" id="yRn-1S-96w"/>