Merge branch 'develop' into voip_design_updates
16
CHANGES.rst
|
@ -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
|
||||
*
|
||||
|
|
6
Riot/Assets/Images.xcassets/Room/Actions/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
26
Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera.png
vendored
Normal file
After Width: | Height: | Size: 469 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera@2x.png
vendored
Normal file
After Width: | Height: | Size: 780 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
26
Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file.png
vendored
Normal file
After Width: | Height: | Size: 653 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.8 KiB |
26
Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library.png
vendored
Normal file
After Width: | Height: | Size: 519 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library@2x.png
vendored
Normal file
After Width: | Height: | Size: 851 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
26
Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker.png
vendored
Normal file
After Width: | Height: | Size: 582 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@2x.png
vendored
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
|
@ -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.";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
146
Riot/Modules/DotsView/DotsView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
// we may have changed the number of bubbles in this block, consider that change
|
||||
return bubbles.count;
|
||||
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;
|
||||
}
|
||||
|
||||
// leave it as is, if coming as 0 from super
|
||||
return count;
|
||||
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
|
||||
|
|
34
Riot/Modules/Room/DataSources/TypingUserInfo.h
Normal 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
|
46
Riot/Modules/Room/DataSources/TypingUserInfo.m
Normal 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
|
|
@ -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;
|
||||
|
||||
// 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++)
|
||||
RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
|
||||
BOOL needsUpdate = currentTypingUsers.count != roomDataSource.currentTypingUsers.count;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
titleView.typingNotificationString = text;
|
||||
if (self.isScrollToBottomHidden
|
||||
&& !self.bubblesTableView.isDragging
|
||||
&& !self.bubblesTableView.isDecelerating)
|
||||
{
|
||||
NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
|
||||
if (count)
|
||||
{
|
||||
[self scrollBubblesTableViewToBottomAnimated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
143
Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift
Normal 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
|
||||
}
|
||||
}
|
48
Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.xib
Normal 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>
|
30
Riot/Modules/Room/Views/InputToolbar/RoomActionItem.swift
Normal 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()
|
||||
}
|
||||
}
|
132
Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -32,3 +32,4 @@
|
|||
#import "AuthFallBackViewController.h"
|
||||
#import "CallViewController.h"
|
||||
#import "MatrixContactsDataSource.h"
|
||||
#import "TypingUserInfo.h"
|
||||
|
|