Merge branch 'develop' into voip_design_updates

This commit is contained in:
ismailgulek 2021-04-09 15:12:51 +03:00
commit 67fa1ab3b8
No known key found for this signature in database
GPG key ID: E96336D42D9470A9
37 changed files with 1055 additions and 282 deletions

View file

@ -2,15 +2,27 @@ Changes to be released in next version
=================================================
✨ Features
*
* Composer Update - Typing and sending a message (#4085)
* Switching composer between text mode & action mode (#4087)
* Explore typing notifications inspired by web (#4134)
🙌 Improvements
* Make the application settings more configurable (#4171)
* Possibility to lock some room creation parameters from settings (#4181)
* Enable / disable external friends invite (#4173)
* Composer update - UI enhancements (#4133)
* Increase grow/shrink animation speed in new composer (#4187)
* Limit typing notifications timeline jumps (#4176)
* Consider displaying names in typing notifications (#4175)
🐛 Bugfix
*
* If you start typing while the new attachment sending mode is on, the send button appears (#4155)
* The final frames of the appearance animation of the new composer buttons are missing (#4160)
* Crash in [RoomViewController setupActions] (#4162)
* Too much vertical whitespace when replying (#4164)
* Black theme uses dark background for composer (#4192)
* Vertical layout of typing notifs can go wonky (#4159)
* Crash in [RoomViewController refreshTypingNotification] (#4161)
⚠️ API Changes
*

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "action_camera.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "action_camera@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "action_camera@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "action_file.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "action_file@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "action_file@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "action_media_library.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "action_media_library@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "action_media_library@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "action_sticker.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "action_sticker@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "action_sticker@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -407,6 +407,8 @@ Tap the + to start adding people.";
"external_link_confirmation_title" = "Double-check this link";
"external_link_confirmation_message" = "The link %@ is taking you to another site: %@\n\nAre you sure you want to continue?";
"room_multiple_typing_notification" = "%@ and others";
// Unknown devices
"unknown_devices_alert_title" = "Room contains unknown sessions";
"unknown_devices_alert" = "This room contains unknown sessions which have not been verified.\nThis means there is no guarantee that the sessions belong to the users they claim to.\nWe recommend you go through the verification process for each session before continuing, but you can resend the message without verifying if you prefer.";

View file

@ -99,6 +99,10 @@ internal enum Asset {
internal static let peopleEmptyScreenArtwork = ImageAsset(name: "people_empty_screen_artwork")
internal static let peopleEmptyScreenArtworkDark = ImageAsset(name: "people_empty_screen_artwork_dark")
internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action")
internal static let actionCamera = ImageAsset(name: "action_camera")
internal static let actionFile = ImageAsset(name: "action_file")
internal static let actionMediaLibrary = ImageAsset(name: "action_media_library")
internal static let actionSticker = ImageAsset(name: "action_sticker")
internal static let error = ImageAsset(name: "error")
internal static let errorMessageTick = ImageAsset(name: "error_message_tick")
internal static let roomActivitiesRetry = ImageAsset(name: "room_activities_retry")

View file

@ -2978,6 +2978,10 @@ internal enum VectorL10n {
internal static var roomMessageUnableOpenLinkErrorMessage: String {
return VectorL10n.tr("Vector", "room_message_unable_open_link_error_message")
}
/// %@ and others
internal static func roomMultipleTypingNotification(_ p1: String) -> String {
return VectorL10n.tr("Vector", "room_multiple_typing_notification", p1)
}
/// %d new message
internal static func roomNewMessageNotification(_ p1: Int) -> String {
return VectorL10n.tr("Vector", "room_new_message_notification", p1)

View file

@ -0,0 +1,146 @@
//
// 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
@IBDesignable
@objcMembers
class DotsView: UIView {
// MARK: - Public properties
@IBInspectable var highlightedDotColor: UIColor = .darkGray
@IBInspectable var dotColor: UIColor = .lightGray
@IBInspectable var dotMaxWidth: CGFloat = 10 {
didSet {
self.sizeToFit()
}
}
@IBInspectable var dotMinWidth: CGFloat = 8 {
didSet {
self.sizeToFit()
}
}
@IBInspectable var numberOfDots: UInt = 3 {
didSet {
createDotViews()
}
}
@IBInspectable var interSpaceMargin: CGFloat = 7 {
didSet {
self.sizeToFit()
}
}
// MARK: - Private members
private var dotLayers: Array<CALayer> = Array()
private var highlightedDotIndex: UInt = 0 {
didSet {
updateDotViews()
}
}
private let updateInterval: TimeInterval = 0.4
private var lastUpdateDate: Date = Date()
private var animating: Bool = false {
didSet {
let displayLink = CADisplayLink(target: self, selector: #selector(fireTimer))
displayLink.add(to: .current, forMode: .default)
}
}
// MARK: - Lifecycle
required init?(coder: NSCoder) {
super.init(coder: coder)
createDotViews()
}
override init(frame: CGRect) {
super.init(frame: frame)
createDotViews()
}
override func layoutSubviews() {
super.layoutSubviews()
updateDotViews()
}
override var intrinsicContentSize: CGSize {
return CGSize(width: dotMaxWidth + (CGFloat(numberOfDots) - 1) * (dotMinWidth + interSpaceMargin), height: dotMaxWidth)
}
override func didMoveToSuperview() {
animating = superview != nil
}
// MARK: - Interface Builder
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
createDotViews()
}
// MARK: - Private methods
private func createDotViews() {
while dotLayers.count > numberOfDots {
dotLayers.popLast()?.removeFromSuperlayer()
}
while dotLayers.count < numberOfDots {
let dotLayer = CALayer()
dotLayer.masksToBounds = true
layer.addSublayer(dotLayer)
dotLayers.append(dotLayer)
}
if highlightedDotIndex >= dotLayers.count {
highlightedDotIndex = 0
updateDotViews()
}
}
private func updateDotViews() {
CATransaction.begin()
CATransaction.setAnimationDuration(1)
var x: CGFloat = 0
for (index, dotLayer) in dotLayers.enumerated() {
if index == highlightedDotIndex {
dotLayer.frame = CGRect(x: x, y: (bounds.height - dotMaxWidth) / 2, width: dotMaxWidth, height: dotMaxWidth)
dotLayer.backgroundColor = dotColor.cgColor
} else {
dotLayer.frame = CGRect(x: x, y: (bounds.height - dotMinWidth) / 2, width: dotMinWidth, height: dotMinWidth)
dotLayer.backgroundColor = index == ((highlightedDotIndex + 1) % numberOfDots) ? highlightedDotColor.cgColor : dotColor.cgColor
}
dotLayer.cornerRadius = dotLayer.bounds.height / 2
x = dotLayer.frame.maxX + interSpaceMargin
}
lastUpdateDate = Date()
CATransaction.commit()
}
@objc private func fireTimer() {
if Date().timeIntervalSince(lastUpdateDate) >= updateInterval {
self.highlightedDotIndex = (self.highlightedDotIndex + 1) % self.numberOfDots
}
}
}

View file

@ -21,6 +21,8 @@
#import "MXRoomSummary+Riot.h"
#import "TypingUserInfo.h"
@protocol RoomDataSourceDelegate;
/**
@ -48,6 +50,11 @@
*/
@property(nonatomic, readonly) RoomEncryptionTrustLevel encryptionTrustLevel;
/**
List of members who are typing in the room.
*/
@property(nonatomic, nullable) NSArray<TypingUserInfo *> *currentTypingUsers;
/**
Check if there is an active jitsi widget in the room and return it.
@ -93,6 +100,8 @@
success:(void(^)(void))success
failure:(void(^)(NSError*))failure;
- (void)resetTypingNotification;
@end
@protocol RoomDataSourceDelegate <MXKDataSourceDelegate>

View file

@ -27,6 +27,7 @@
#import "MXRoom+Riot.h"
const CGFloat kTypingCellHeight = 24;
@interface RoomDataSource() <BubbleReactionsViewModelDelegate>
{
@ -53,6 +54,8 @@
@property (nonatomic) BOOL showRoomCreationCell;
@property (nonatomic) NSInteger typingCellIndex;
@end
@implementation RoomDataSource
@ -185,6 +188,16 @@
[self setNeedsUpdateAdditionalContentHeightForCellData:cellData];
}
- (CGFloat)cellHeightAtIndex:(NSInteger)index withMaximumWidth:(CGFloat)maxWidth
{
if (index == self.typingCellIndex)
{
return kTypingCellHeight;
}
return [super cellHeightAtIndex:index withMaximumWidth:maxWidth];
}
- (void)setNeedsUpdateAdditionalContentHeightForCellData:(id<MXKRoomBubbleCellDataStoring>)cellData
{
RoomBubbleCellData *roomBubbleCellData;
@ -261,16 +274,40 @@
[self updateStatusInfo];
}
if (!self.currentTypingUsers)
{
self.typingCellIndex = -1;
// we may have changed the number of bubbles in this block, consider that change
return bubbles.count;
}
self.typingCellIndex = bubbles.count;
return bubbles.count + 1;
}
if (!self.currentTypingUsers)
{
self.typingCellIndex = -1;
// leave it as is, if coming as 0 from super
return count;
}
self.typingCellIndex = count;
return count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == self.typingCellIndex)
{
RoomTypingBubbleCell *cell = [tableView dequeueReusableCellWithIdentifier:RoomTypingBubbleCell.defaultReuseIdentifier forIndexPath:indexPath];
[cell updateWithTheme:ThemeService.shared.theme];
[cell updateTypingUsers:_currentTypingUsers mediaManager:self.mxSession.mediaManager];
return cell;
}
// Do cell data customization that needs to be done before [MXKRoomBubbleTableViewCell render]
RoomBubbleCellData *roomBubbleCellData = [self cellDataAtIndex:indexPath.row];
@ -917,6 +954,10 @@
}];
}
- (void)resetTypingNotification {
self.currentTypingUsers = nil;
}
#pragma - Accessibility
- (void)setupAccessibilityForCell:(MXKRoomBubbleTableViewCell *)cell withCellData:(RoomBubbleCellData*)cellData

View file

@ -0,0 +1,34 @@
//
// 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 <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
NS_ASSUME_NONNULL_BEGIN
@interface TypingUserInfo : NSObject
@property (nonatomic, strong) NSString *userId;
@property (nonatomic, strong, nullable) NSString *displayName;
@property (nonatomic, strong, nullable) NSString *avatarUrl;
- (instancetype) initWithMember:(MXRoomMember*)member;
- (instancetype) initWithUserId:(NSString*)userId;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,46 @@
//
// 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 "TypingUserInfo.h"
@implementation TypingUserInfo
- (instancetype) initWithMember:(MXRoomMember*)member
{
self = [self initWithUserId:member.userId];
if (self)
{
self.displayName = member.displayname;
self.avatarUrl = member.avatarUrl;
}
return self;
}
- (instancetype) initWithUserId:(NSString*)userId
{
self = [super init];
if (self)
{
self.userId = userId;
}
return self;
}
@end

View file

@ -124,12 +124,15 @@
#import "SettingsViewController.h"
#import "SecurityViewController.h"
#import "TypingUserInfo.h"
#import "Riot-Swift.h"
NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification";
NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification";
NSNotificationName const RoomViewControllerViewDidAppearNotification = @"RoomViewControllerViewDidAppearNotification";
NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"RoomViewControllerViewDidDisappearNotification";
const NSTimeInterval kResizeComposerAnimationDuration = .05;
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, RoomParticipantsViewControllerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
@ -295,6 +298,8 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
{
[super finalizeInit];
self.resizeComposerAnimationDuration = kResizeComposerAnimationDuration;
// Setup `MXKViewControllerHandling` properties
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
@ -380,6 +385,8 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
[self.bubblesTableView registerClass:RoomCreationIntroCell.class forCellReuseIdentifier:RoomCreationIntroCell.defaultReuseIdentifier];
[self.bubblesTableView registerNib:RoomTypingBubbleCell.nib forCellReuseIdentifier:RoomTypingBubbleCell.defaultReuseIdentifier];
[self vc_removeBackTitle];
// Replace the default input toolbar view.
@ -420,6 +427,8 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
}];
[self userInterfaceThemeDidChange];
[self setupActions];
}
- (void)userInterfaceThemeDidChange
@ -471,7 +480,7 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
self.scrollToBottomButton.layer.shadowRadius = 6;
self.scrollToBottomButton.layer.shadowOffset = CGSizeMake(0, 4);
self.inputBackgroundView.backgroundColor = [ThemeService.shared.theme.searchBackgroundColor colorWithAlphaComponent:0.98];
self.inputBackgroundView.backgroundColor = [ThemeService.shared.theme.backgroundColor colorWithAlphaComponent:0.98];
if ([ThemeService.shared.themeId isEqualToString:@"light"])
{
@ -517,6 +526,12 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
[self refreshRoomInputToolbar];
}
// Reset typing notification in order to remove the allocated space
if ([self.roomDataSource isKindOfClass:RoomDataSource.class])
{
[((RoomDataSource*)self.roomDataSource) resetTypingNotification];
}
[self listenTypingNotifications];
[self listenCallNotifications];
[self listenWidgetNotifications];
@ -1370,6 +1385,17 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
_scrollToBottomHidden = scrollToBottomHidden;
}
if (!_scrollToBottomHidden && [self.roomDataSource isKindOfClass:RoomDataSource.class])
{
RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
if (roomDataSource.currentTypingUsers && !roomDataSource.currentTypingUsers.count)
{
[roomDataSource resetTypingNotification];
NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
[self.bubblesTableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
}
}
[UIView animateWithDuration:.2 animations:^{
self.scrollToBottomBadgeLabel.alpha = (scrollToBottomHidden || !self.scrollToBottomBadgeLabel.text) ? 0 : 1;
self.scrollToBottomButton.alpha = scrollToBottomHidden ? 0 : 1;
@ -1738,6 +1764,122 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
[self.roomInfoCoordinatorBridgePresenter pushFrom:self.navigationController animated:YES];
}
- (void)setupActions {
if (![self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
return;
}
RoomInputToolbarView *roomInputView = ((RoomInputToolbarView *) self.inputToolbarView);
MXWeakify(self);
roomInputView.actionsBar.actionItems = @[
[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{
MXStrongifyAndReturnIfNil(self);
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
}
[self showCameraControllerAnimated:YES];
}],
[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_media_library"] andAction:^{
MXStrongifyAndReturnIfNil(self);
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
}
[self showMediaPickerAnimated:YES];
}],
[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_sticker"] andAction:^{
MXStrongifyAndReturnIfNil(self);
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
}
[self roomInputToolbarViewPresentStickerPicker];
}],
[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_file"] andAction:^{
MXStrongifyAndReturnIfNil(self);
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
}
[self roomInputToolbarViewDidTapFileUpload];
}],
];
}
- (void)roomInputToolbarViewPresentStickerPicker
{
// Search for the sticker picker widget in the user account
Widget *widget = [[WidgetManager sharedManager] userWidgets:self.roomDataSource.mxSession ofTypes:@[kWidgetTypeStickerPicker]].firstObject;
if (widget)
{
// Display the widget
[widget widgetUrl:^(NSString * _Nonnull widgetUrl) {
StickerPickerViewController *stickerPickerVC = [[StickerPickerViewController alloc] initWithUrl:widgetUrl forWidget:widget];
stickerPickerVC.roomDataSource = self.roomDataSource;
[self.navigationController pushViewController:stickerPickerVC animated:YES];
} failure:^(NSError * _Nonnull error) {
NSLog(@"[RoomVC] Cannot display widget %@", widget);
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
else
{
// The Sticker picker widget is not installed yet. Propose the user to install it
MXWeakify(self);
[currentAlert dismissViewControllerAnimated:NO completion:nil];
NSString *alertMessage = [NSString stringWithFormat:@"%@\n%@",
NSLocalizedStringFromTable(@"widget_sticker_picker_no_stickerpacks_alert", @"Vector", nil),
NSLocalizedStringFromTable(@"widget_sticker_picker_no_stickerpacks_alert_add_now", @"Vector", nil)
];
currentAlert = [UIAlertController alertControllerWithTitle:nil message:alertMessage preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action)
{
MXStrongifyAndReturnIfNil(self);
self->currentAlert = nil;
}]];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
MXStrongifyAndReturnIfNil(self);
self->currentAlert = nil;
// Show the sticker picker settings screen
IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc]
initForMXSession:self.roomDataSource.mxSession
inRoom:self.roomDataSource.roomId
screen:[IntegrationManagerViewController screenForWidget:kWidgetTypeStickerPicker]
widgetId:nil];
[self presentViewController:modularVC animated:NO completion:nil];
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCStickerPickerAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
}
- (void)roomInputToolbarViewDidTapFileUpload
{
MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new];
documentPickerPresenter.delegate = self;
NSArray<MXKUTI*> *allowedUTIs = @[MXKUTI.data];
[documentPickerPresenter presentDocumentPickerWith:allowedUTIs from:self animated:YES completion:nil];
self.documentPickerPresenter = documentPickerPresenter;
}
#pragma mark - Dialpad
- (void)openDialpad
@ -3437,80 +3579,6 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
}
#pragma mark - RoomInputToolbarViewDelegate
- (void)roomInputToolbarViewPresentStickerPicker:(MXKRoomInputToolbarView*)toolbarView
{
// Search for the sticker picker widget in the user account
Widget *widget = [[WidgetManager sharedManager] userWidgets:self.roomDataSource.mxSession ofTypes:@[kWidgetTypeStickerPicker]].firstObject;
if (widget)
{
// Display the widget
[widget widgetUrl:^(NSString * _Nonnull widgetUrl) {
StickerPickerViewController *stickerPickerVC = [[StickerPickerViewController alloc] initWithUrl:widgetUrl forWidget:widget];
stickerPickerVC.roomDataSource = self.roomDataSource;
[self.navigationController pushViewController:stickerPickerVC animated:YES];
} failure:^(NSError * _Nonnull error) {
NSLog(@"[RoomVC] Cannot display widget %@", widget);
[[AppDelegate theDelegate] showErrorAsAlert:error];
}];
}
else
{
// The Sticker picker widget is not installed yet. Propose the user to install it
__weak typeof(self) weakSelf = self;
[currentAlert dismissViewControllerAnimated:NO completion:nil];
NSString *alertMessage = [NSString stringWithFormat:@"%@\n%@",
NSLocalizedStringFromTable(@"widget_sticker_picker_no_stickerpacks_alert", @"Vector", nil),
NSLocalizedStringFromTable(@"widget_sticker_picker_no_stickerpacks_alert_add_now", @"Vector", nil)
];
currentAlert = [UIAlertController alertControllerWithTitle:nil message:alertMessage preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"]
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
// Show the sticker picker settings screen
IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc]
initForMXSession:self.roomDataSource.mxSession
inRoom:self.roomDataSource.roomId
screen:[IntegrationManagerViewController screenForWidget:kWidgetTypeStickerPicker]
widgetId:nil];
[self presentViewController:modularVC animated:NO completion:nil];
}
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCStickerPickerAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
}
#pragma mark - VoIP
- (void)placeCallWithVideo:(BOOL)video
@ -3757,27 +3825,6 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
}
}
- (void)roomInputToolbarViewDidTapFileUpload:(MXKRoomInputToolbarView *)toolbarView
{
MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new];
documentPickerPresenter.delegate = self;
NSArray<MXKUTI*> *allowedUTIs = @[MXKUTI.data];
[documentPickerPresenter presentDocumentPickerWith:allowedUTIs from:self animated:YES completion:nil];
self.documentPickerPresenter = documentPickerPresenter;
}
- (void)roomInputToolbarViewDidTapCamera:(MXKRoomInputToolbarView*)toolbarView
{
[self showCameraControllerAnimated:YES];
}
- (void)roomInputToolbarViewDidTapMediaLibrary:(MXKRoomInputToolbarView*)toolbarView
{
[self showMediaPickerAnimated:YES];
}
- (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView*)toolbarView
{
[self cancelEventSelection];
@ -4181,54 +4228,51 @@ NSNotificationName const RoomViewControllerViewDidDisappearNotification = @"Room
- (void)refreshTypingNotification
{
if ([self.titleView isKindOfClass:RoomTitleView.class])
{
RoomTitleView *titleView = (RoomTitleView *)self.titleView;
RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
BOOL needsUpdate = currentTypingUsers.count != roomDataSource.currentTypingUsers.count;
// Prepare here typing notification
NSString* text = nil;
NSUInteger count = currentTypingUsers.count;
// get the room member names
NSMutableArray *names = [[NSMutableArray alloc] init];
// keeps the only the first two users
for(int i = 0; i < MIN(count, 2); i++)
NSMutableArray *typingUsers = [NSMutableArray new];
for (NSUInteger i = 0 ; i < currentTypingUsers.count ; i++) {
NSString *userId = currentTypingUsers[i];
MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:userId];
TypingUserInfo *userInfo;
if (member)
{
NSString* name = currentTypingUsers[i];
MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:name];
if (member && member.displayname.length)
{
name = member.displayname;
}
// sanity check
if (name)
{
[names addObject:name];
}
}
if (0 == names.count)
{
// something to do ?
}
else if (1 == names.count)
{
text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_one_user_is_typing", @"Vector", nil), names[0]];
}
else if (2 == names.count)
{
text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_two_users_are_typing", @"Vector", nil), names[0], names[1]];
userInfo = [[TypingUserInfo alloc] initWithMember: member];
}
else
{
text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_many_users_are_typing", @"Vector", nil), names[0], names[1]];
userInfo = [[TypingUserInfo alloc] initWithUserId: userId];
}
[typingUsers addObject:userInfo];
needsUpdate = needsUpdate || userInfo.userId != ((MXRoomMember *) roomDataSource.currentTypingUsers[i]).userId;
}
titleView.typingNotificationString = text;
if (needsUpdate)
{
BOOL needsReload = roomDataSource.currentTypingUsers == nil;
roomDataSource.currentTypingUsers = typingUsers;
if (needsReload)
{
[self.bubblesTableView reloadData];
}
else
{
NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:count - 1 inSection:0];
[self.bubblesTableView reloadRowsAtIndexPaths:@[lastIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
if (self.isScrollToBottomHidden
&& !self.bubblesTableView.isDragging
&& !self.bubblesTableView.isDecelerating)
{
NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
if (count)
{
[self scrollBubblesTableViewToBottomAnimated:YES];
}
}
}
}

View file

@ -0,0 +1,143 @@
//
// 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
class RoomTypingBubbleCell: MXKTableViewCell, Themable {
// MARK: - Constants
private enum Constants {
static let maxPictureCount = 4
static let pictureSize: CGFloat = 24
static let pictureMaxMargin: CGFloat = 16
static let pictureMinMargin: CGFloat = 8
}
// MARK: - Outlets
@IBOutlet private weak var additionalUsersLabel: UILabel!
@IBOutlet private weak var additionalUsersLabelLeadingConstraint: NSLayoutConstraint!
@IBOutlet private weak var dotsView: DotsView!
@IBOutlet private weak var dotsViewLeadingConstraint: NSLayoutConstraint!
// MARK: - members
private var userPictureViews: [MXKImageView] = []
// MARK: - Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
update(theme: ThemeService.shared().theme)
}
override func prepareForReuse() {
super.prepareForReuse()
for pictureView in userPictureViews {
pictureView.removeFromSuperview()
}
}
override func layoutSubviews() {
super.layoutSubviews()
dotsView.isHidden = userPictureViews.count == 0
guard userPictureViews.count > 0 else {
return
}
additionalUsersLabel?.sizeToFit()
var pictureViewsMaxX: CGFloat = 0
var xOffset: CGFloat = 0
for pictureView in userPictureViews {
pictureView.center = CGPoint(x: Constants.pictureMaxMargin + xOffset + pictureView.bounds.midX, y: self.bounds.midY)
xOffset += round(pictureView.bounds.maxX * 2 / 3)
pictureViewsMaxX = pictureView.frame.maxX
}
let leftMagin: CGFloat = pictureViewsMaxX + (userPictureViews.count == 1 ? Constants.pictureMaxMargin : Constants.pictureMinMargin)
additionalUsersLabelLeadingConstraint.constant = leftMagin
dotsViewLeadingConstraint?.constant = additionalUsersLabel.text.isEmptyOrNil == true ? leftMagin : leftMagin + 8 + additionalUsersLabel.frame.width
}
// MARK: - Overrides
override class func defaultReuseIdentifier() -> String {
return String(describing: self)
}
override class func nib() -> UINib {
return UINib(nibName: String(describing: self), bundle: nil)
}
// MARK: - Themable
func update(theme: Theme) {
additionalUsersLabel.textColor = theme.textSecondaryColor
dotsView.highlightedDotColor = theme.textTertiaryColor
dotsView.dotColor = theme.textSecondaryColor
}
// MARK: - Business methods
func updateTypingUsers(_ typingUsers: [TypingUserInfo], mediaManager: MXMediaManager) {
for pictureView in userPictureViews {
pictureView.removeFromSuperview()
}
userPictureViews = []
for user in typingUsers {
if userPictureViews.count >= Constants.maxPictureCount {
break
}
let pictureView = MXKImageView(frame: CGRect(x: 0, y: 0, width: Constants.pictureSize, height: Constants.pictureSize))
pictureView.layer.masksToBounds = true
pictureView.layer.cornerRadius = pictureView.bounds.midX
let defaultavatarImage = AvatarGenerator.generateAvatar(forMatrixItem: user.userId, withDisplayName: user.displayName)
pictureView.setImageURI(user.avatarUrl, withType: nil, andImageOrientation: .up, toFitViewSize: pictureView.bounds.size, with: MXThumbnailingMethodCrop, previewImage: defaultavatarImage, mediaManager: mediaManager)
userPictureViews.append(pictureView)
self.contentView.addSubview(pictureView)
}
switch typingUsers.count {
case 0:
additionalUsersLabel.text = nil
case 1:
additionalUsersLabel.text = firstUserNameFor(typingUsers)
default:
additionalUsersLabel.text = VectorL10n.roomMultipleTypingNotification(firstUserNameFor(typingUsers) ?? "")
}
self.setNeedsLayout()
}
private func firstUserNameFor(_ typingUsers: Array<TypingUserInfo>) -> String? {
guard let firstUser = typingUsers.first else {
return nil
}
return firstUser.displayName.isEmptyOrNil ? firstUser.userId : firstUser.displayName
}
}

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="nQB-23-kip" customClass="RoomTypingBubbleCell" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="nQB-23-kip" id="vRo-3W-dC9">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="+3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4KE-u0-T4p">
<rect key="frame" x="20" y="13" width="18.5" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Bn5-WN-DQs" customClass="DotsView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="38" y="13" width="91" height="18"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
<constraints>
<constraint firstItem="4KE-u0-T4p" firstAttribute="leading" secondItem="vRo-3W-dC9" secondAttribute="leading" constant="20" symbolic="YES" id="PFN-Ig-2NK"/>
<constraint firstItem="Bn5-WN-DQs" firstAttribute="leading" secondItem="vRo-3W-dC9" secondAttribute="leading" constant="38" id="hmq-dH-neQ"/>
<constraint firstItem="Bn5-WN-DQs" firstAttribute="centerY" secondItem="vRo-3W-dC9" secondAttribute="centerY" id="jup-hc-eNQ"/>
</constraints>
</tableViewCellContentView>
<constraints>
<constraint firstItem="4KE-u0-T4p" firstAttribute="centerY" secondItem="nQB-23-kip" secondAttribute="centerY" id="hBP-OB-KGd"/>
</constraints>
<connections>
<outlet property="additionalUsersLabel" destination="4KE-u0-T4p" id="SVG-Oa-aHI"/>
<outlet property="additionalUsersLabelLeadingConstraint" destination="PFN-Ig-2NK" id="4Wr-XS-XXp"/>
<outlet property="dotsView" destination="Bn5-WN-DQs" id="QqF-bu-Pbm"/>
<outlet property="dotsViewLeadingConstraint" destination="hmq-dH-neQ" id="6hM-Sc-pCc"/>
</connections>
<point key="canvasLocation" x="166.66666666666669" y="20.758928571428569"/>
</tableViewCell>
</objects>
</document>

View file

@ -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
class RoomActionItem: NSObject {
let image: UIImage
let action: (() -> Void)
init(image: UIImage, andAction action: @escaping () -> Void) {
self.image = image
self.action = action
super.init()
}
}

View file

@ -0,0 +1,132 @@
//
// 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
class RoomActionsBar: UIScrollView, Themable {
// MARK: - Properties
var itemSpacing: CGFloat = 20 {
didSet {
self.setNeedsLayout()
}
}
var actionItems: [RoomActionItem] = [] {
didSet {
var actionButtons: [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: [UIButton] = [] {
willSet {
for button in actionButtons {
button.removeFromSuperview()
}
}
}
private var lastBounds = CGRect.zero
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
override func layoutSubviews() {
super.layoutSubviews()
guard lastBounds != self.bounds else {
return
}
lastBounds = self.bounds
var currentX: CGFloat = 0
for button in actionButtons {
button.transform = CGAffineTransform.identity
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: - Themable
func update(theme: Theme) {
for button in actionButtons {
button.tintColor = theme.tintColor
}
}
// MARK: - Business methods
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.3, delay: 0.05 * Double(index), usingSpringWithDamping: 0.45, initialSpringVelocity: 11, options: .curveEaseInOut) {
button.transform = CGAffineTransform.identity
} completion: { (finished) in
completion?(finished)
}
}
} else {
for (index, button) in actionButtons.enumerated() {
UIView.animate(withDuration: 0.25, delay: 0.05 * Double(index), options: .curveEaseInOut) {
button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height)
} completion: { (finished) in
if index == self.actionButtons.count - 1 {
completion?(finished)
}
}
}
}
}
// MARK: - Private methods
@objc private func buttonAction(_ sender: UIButton) {
actionItems[sender.tag].action()
}
private func setupView() {
self.showsHorizontalScrollIndicator = false
}
}

View file

@ -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

View file

@ -27,7 +27,13 @@
#import "WidgetManager.h"
#import "IntegrationManagerViewController.h"
const double RoomInputToolbarViewContextBarHeight = 30;
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;
@interface RoomInputToolbarView()
{
@ -120,6 +126,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 updateWithTheme:ThemeService.shared.theme];
}
#pragma mark -
@ -142,6 +149,7 @@ const double RoomInputToolbarViewContextBarHeight = 30;
RoomInputToolbarViewSendMode previousMode = _sendMode;
_sendMode = sendMode;
self.actionMenuOpened = NO;
[self updatePlaceholder];
[self updateToolbarButtonLabelWithPreviousMode: previousMode];
}
@ -159,26 +167,26 @@ const double RoomInputToolbarViewContextBarHeight = 30;
self.inputContextImageView.image = [UIImage imageNamed:@"input_reply_icon"];
self.inputContextLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_message_replying_to", @"Vector", nil), self.eventSenderDisplayName];
self.inputContextViewHeightConstraint.constant = RoomInputToolbarViewContextBarHeight;
updatedHeight += RoomInputToolbarViewContextBarHeight;
self->growingTextView.maxHeight -= RoomInputToolbarViewContextBarHeight;
self.inputContextViewHeightConstraint.constant = kContextBarHeight;
updatedHeight += kContextBarHeight;
self->growingTextView.maxHeight -= kContextBarHeight;
break;
case RoomInputToolbarViewSendModeEdit:
buttonImage = [UIImage imageNamed:@"save_icon"];
self.inputContextImageView.image = [UIImage imageNamed:@"input_edit_icon"];
self.inputContextLabel.text = NSLocalizedStringFromTable(@"room_message_editing", @"Vector", nil);
self.inputContextViewHeightConstraint.constant = RoomInputToolbarViewContextBarHeight;
updatedHeight += RoomInputToolbarViewContextBarHeight;
self->growingTextView.maxHeight -= RoomInputToolbarViewContextBarHeight;
self.inputContextViewHeightConstraint.constant = kContextBarHeight;
updatedHeight += kContextBarHeight;
self->growingTextView.maxHeight -= kContextBarHeight;
break;
default:
buttonImage = [UIImage imageNamed:@"send_icon"];
if (previousMode != _sendMode)
{
updatedHeight -= RoomInputToolbarViewContextBarHeight;
self->growingTextView.maxHeight += RoomInputToolbarViewContextBarHeight;
updatedHeight -= kContextBarHeight;
self->growingTextView.maxHeight += kContextBarHeight;
}
self.inputContextViewHeightConstraint.constant = 0;
break;
@ -199,7 +207,7 @@ const double RoomInputToolbarViewContextBarHeight = 30;
if (self.mainToolbarHeightConstraint.constant != updatedHeight)
{
[UIView animateWithDuration:.3 animations:^{
[UIView animateWithDuration:kSendModeAnimationDuration animations:^{
self.mainToolbarHeightConstraint.constant = updatedHeight;
[self layoutIfNeeded];
@ -329,92 +337,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];
@ -433,6 +356,8 @@ const double RoomInputToolbarViewContextBarHeight = 30;
- (void)updateSendButtonWithMessage:(NSString *)textMessage
{
self.actionMenuOpened = NO;
if (textMessage.length)
{
self.rightInputToolbarButton.alpha = 1;
@ -443,9 +368,61 @@ 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 (self->growingTextView.internalTextView.selectedRange.length > 0)
{
NSRange range = self->growingTextView.internalTextView.selectedRange;
range.location = range.location + range.length;
range.length = 0;
self->growingTextView.internalTextView.selectedRange = range;
}
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:kActionMenuAttachButtonAnimationDuration delay:0 usingSpringWithDamping:kActionMenuAttachButtonSpringDamping initialSpringVelocity:kActionMenuAttachButtonSpringVelocity options:UIViewAnimationOptionCurveEaseIn animations:^{
self.attachMediaButton.transform = actionMenuOpened ? CGAffineTransformMakeRotation(M_PI * 3 / 4) : CGAffineTransformIdentity;
} completion:nil];
[UIView animateWithDuration:kActionMenuContentAlphaAnimationDuration 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:nil];
[UIView animateWithDuration:kActionMenuComposerHeightAnimationDuration 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

View file

@ -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>
@ -37,16 +45,16 @@
<rect key="frame" x="0.0" y="0.0" width="528" height="32"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="input_edit_icon" translatesAutoresizingMaskIntoConstraints="NO" id="PZ4-0Y-TmL">
<rect key="frame" x="12" y="11" width="10.5" height="10"/>
<rect key="frame" x="8" y="16" width="10.5" height="10"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dVr-ZM-kkX">
<rect key="frame" x="26.5" y="9" width="461.5" height="14.5"/>
<rect key="frame" x="22.5" y="13.5" width="471.5" height="14.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="48y-kn-7b5">
<rect key="frame" x="492" y="1" width="30" height="30"/>
<rect key="frame" x="498" y="6" width="30" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="I17-S0-9fp"/>
<constraint firstAttribute="width" constant="30" id="cCe-RB-ET2"/>
@ -59,30 +67,30 @@
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="dVr-ZM-kkX" secondAttribute="bottom" constant="4" id="58M-yd-hhK"/>
<constraint firstItem="PZ4-0Y-TmL" firstAttribute="centerY" secondItem="dVr-ZM-kkX" secondAttribute="centerY" id="Bu1-sl-qDk"/>
<constraint firstAttribute="height" constant="32" id="KNn-ng-NHK"/>
<constraint firstItem="dVr-ZM-kkX" firstAttribute="leading" secondItem="PZ4-0Y-TmL" secondAttribute="trailing" constant="4" id="RbN-mc-y2P"/>
<constraint firstItem="48y-kn-7b5" firstAttribute="centerY" secondItem="jXI-9E-Bgl" secondAttribute="centerY" id="XbN-rm-nDw"/>
<constraint firstItem="48y-kn-7b5" firstAttribute="leading" secondItem="dVr-ZM-kkX" secondAttribute="trailing" constant="4" id="bmi-rg-TNM"/>
<constraint firstItem="PZ4-0Y-TmL" firstAttribute="centerY" secondItem="jXI-9E-Bgl" secondAttribute="centerY" id="f9O-vU-41g"/>
<constraint firstItem="PZ4-0Y-TmL" firstAttribute="leading" secondItem="jXI-9E-Bgl" secondAttribute="leading" constant="12" id="mp0-tl-IIe"/>
<constraint firstAttribute="trailing" secondItem="48y-kn-7b5" secondAttribute="trailing" constant="6" id="qPb-EI-csl"/>
<constraint firstItem="dVr-ZM-kkX" firstAttribute="centerY" secondItem="jXI-9E-Bgl" secondAttribute="centerY" id="yb4-bq-XNb"/>
<constraint firstItem="PZ4-0Y-TmL" firstAttribute="leading" secondItem="jXI-9E-Bgl" secondAttribute="leading" constant="8" id="mp0-tl-IIe"/>
<constraint firstAttribute="trailing" secondItem="48y-kn-7b5" secondAttribute="trailing" id="qPb-EI-csl"/>
<constraint firstItem="48y-kn-7b5" firstAttribute="centerY" secondItem="dVr-ZM-kkX" secondAttribute="centerY" id="z5v-Vy-6tc"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wgb-ON-N29" customClass="KeyboardGrowingTextView">
<rect key="frame" x="4" y="33" width="520" height="4"/>
<rect key="frame" x="5" y="33" width="518" height="4"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="GrowingTextView"/>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="wgb-ON-N29" secondAttribute="trailing" constant="4" id="30f-rE-CKj"/>
<constraint firstAttribute="trailing" secondItem="wgb-ON-N29" secondAttribute="trailing" constant="5" id="30f-rE-CKj"/>
<constraint firstAttribute="trailing" secondItem="jXI-9E-Bgl" secondAttribute="trailing" id="3EM-Mc-ZaI"/>
<constraint firstItem="jXI-9E-Bgl" firstAttribute="top" secondItem="QWp-NV-uh5" secondAttribute="top" id="Bp8-45-jvJ"/>
<constraint firstItem="uH7-Q7-hpZ" firstAttribute="leading" secondItem="QWp-NV-uh5" secondAttribute="leading" id="Fli-kz-OcS"/>
<constraint firstItem="uH7-Q7-hpZ" firstAttribute="top" secondItem="QWp-NV-uh5" secondAttribute="top" id="Gqc-ya-F1W"/>
<constraint firstItem="wgb-ON-N29" firstAttribute="leading" secondItem="QWp-NV-uh5" secondAttribute="leading" constant="4" id="N7q-ch-iRz"/>
<constraint firstItem="wgb-ON-N29" firstAttribute="leading" secondItem="QWp-NV-uh5" secondAttribute="leading" constant="5" id="N7q-ch-iRz"/>
<constraint firstItem="wgb-ON-N29" firstAttribute="top" secondItem="jXI-9E-Bgl" secondAttribute="bottom" constant="1" id="UV2-Sh-peE"/>
<constraint firstAttribute="bottom" secondItem="uH7-Q7-hpZ" secondAttribute="bottom" id="dAX-uO-gvm"/>
<constraint firstAttribute="bottom" secondItem="wgb-ON-N29" secondAttribute="bottom" constant="-1" id="fFG-SH-Hjh"/>
@ -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"/>

View file

@ -147,7 +147,6 @@
self.pictureViewHeightConstraint.constant = 28;
self.displayNameTextField.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
self.typingLabel.font = [UIFont systemFontOfSize:10];
self.dotViewCenterXConstraint.constant = 3;
self.dotViewCenterYConstraint.constant = -2;
}
else
@ -158,7 +157,6 @@
self.pictureViewHeightConstraint.constant = 32;
self.displayNameTextField.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium];
self.typingLabel.font = [UIFont systemFontOfSize:12];
self.dotViewCenterXConstraint.constant = 0;
self.dotViewCenterYConstraint.constant = -1;
}
}

View file

@ -71,7 +71,7 @@
<constraint firstItem="6uH-I3-RQg" firstAttribute="leading" secondItem="LDd-c1-ILP" secondAttribute="trailing" constant="12" id="0pG-0z-gpD"/>
<constraint firstItem="LDd-c1-ILP" firstAttribute="centerY" secondItem="BkF-x3-7fX" secondAttribute="centerY" id="33h-dC-S1U"/>
<constraint firstAttribute="bottom" secondItem="sD9-l7-azQ" secondAttribute="bottom" id="4rX-5O-LrO"/>
<constraint firstItem="yTB-Be-bLN" firstAttribute="centerX" secondItem="SUm-iW-DRR" secondAttribute="leading" id="7H4-kh-c2g"/>
<constraint firstItem="yTB-Be-bLN" firstAttribute="centerX" secondItem="SUm-iW-DRR" secondAttribute="trailing" id="7H4-kh-c2g"/>
<constraint firstItem="sD9-l7-azQ" firstAttribute="leading" secondItem="BkF-x3-7fX" secondAttribute="leading" id="AJc-Aa-sht"/>
<constraint firstItem="SUm-iW-DRR" firstAttribute="centerY" secondItem="BkF-x3-7fX" secondAttribute="centerY" id="Blv-SJ-r6v"/>
<constraint firstItem="Ky3-cy-HAx" firstAttribute="bottom" secondItem="LDd-c1-ILP" secondAttribute="bottom" id="HZy-1x-eyX"/>

View file

@ -32,3 +32,4 @@
#import "AuthFallBackViewController.h"
#import "CallViewController.h"
#import "MatrixContactsDataSource.h"
#import "TypingUserInfo.h"