From 2340c575d034b46f9f5fc176d0a6fd9a2b8a1ff0 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 22 Mar 2021 19:55:41 +0100 Subject: [PATCH 01/38] Explore typing notifications inspired by web - First iteration --- Riot/Modules/DotsView/DotsView.swift | 152 ++++++++++++++++++ .../Modules/Room/DataSources/RoomDataSource.h | 5 + .../Modules/Room/DataSources/RoomDataSource.m | 41 ++++- Riot/Modules/Room/RoomViewController.m | 63 +++----- Riot/Modules/Room/RoomViewController.xib | 4 +- .../BubbleCells/RoomTypingBubbleCell.swift | 105 ++++++++++++ .../BubbleCells/RoomTypingBubbleCell.xib | 54 +++++++ 7 files changed, 375 insertions(+), 49 deletions(-) create mode 100644 Riot/Modules/DotsView/DotsView.swift create mode 100644 Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift create mode 100644 Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.xib diff --git a/Riot/Modules/DotsView/DotsView.swift b/Riot/Modules/DotsView/DotsView.swift new file mode 100644 index 000000000..ca69fb6de --- /dev/null +++ b/Riot/Modules/DotsView/DotsView.swift @@ -0,0 +1,152 @@ +// +// 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 { + didSet { + } + } + + @IBInspectable var dotColor: UIColor = .lightGray { + didSet { + } + } + + @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 = 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() + } + + func fireTimer() { + if Date().timeIntervalSince(lastUpdateDate) >= updateInterval { + self.highlightedDotIndex = (self.highlightedDotIndex + 1) % self.numberOfDots + } + } +} diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index f28f91fac..ced9e687a 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -48,6 +48,11 @@ */ @property(nonatomic, readonly) RoomEncryptionTrustLevel encryptionTrustLevel; +/** + List of members who are typing in the room. + */ +@property(nonatomic, nullable) NSArray *currentTypingUsers; + /** Check if there is an active jitsi widget in the room and return it. diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 8975281c8..880f6dc65 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -53,6 +53,8 @@ @property (nonatomic) BOOL showRoomCreationCell; +@property (nonatomic) NSInteger typingCellIndex; + @end @implementation RoomDataSource @@ -185,6 +187,16 @@ [self setNeedsUpdateAdditionalContentHeightForCellData:cellData]; } +- (CGFloat)cellHeightAtIndex:(NSInteger)index withMaximumWidth:(CGFloat)maxWidth +{ + if (index == self.typingCellIndex) + { + return 24; + } + + return [super cellHeightAtIndex:index withMaximumWidth:maxWidth]; +} + - (void)setNeedsUpdateAdditionalContentHeightForCellData:(id)cellData { RoomBubbleCellData *roomBubbleCellData; @@ -261,16 +273,37 @@ [self updateStatusInfo]; } - // we may have changed the number of bubbles in this block, consider that change - return bubbles.count; + if (self.currentTypingUsers.count == 0) + { + // 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.count == 0) + { + 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.cellIdentifier forIndexPath:indexPath]; + [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]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 963fc991f..442a6fd79 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -370,6 +370,8 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo [self.bubblesTableView registerClass:RoomCreationIntroCell.class forCellReuseIdentifier:RoomCreationIntroCell.defaultReuseIdentifier]; + [self.bubblesTableView registerNib:[UINib nibWithNibName:@"RoomTypingBubbleCell" bundle:nil] forCellReuseIdentifier:RoomTypingBubbleCell.cellIdentifier]; + [self vc_removeBackTitle]; // Replace the default input toolbar view. @@ -4025,54 +4027,29 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo - (void)refreshTypingNotification { - if ([self.titleView isKindOfClass:RoomTitleView.class]) + 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]; + [typingUsers addObject:member]; + needsUpdate = needsUpdate || member.userId != ((MXRoomMember *) roomDataSource.currentTypingUsers[i]).userId; + } + + if (needsUpdate) { - 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.currentTypingUsers = typingUsers; + [self.bubblesTableView reloadData]; + if (self.isScrollToBottomHidden) { - NSString* name = currentTypingUsers[i]; - - MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:name]; - - if (member && member.displayname.length) + NSInteger count = [self.roomDataSource tableView:self.bubblesTableView numberOfRowsInSection:0]; + if (count) { - name = member.displayname; - } - - // sanity check - if (name) - { - [names addObject:name]; + [self scrollBubblesTableViewToBottomAnimated:YES]; } } - - 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]]; - } - else - { - text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_many_users_are_typing", @"Vector", nil), names[0], names[1]]; - } - - titleView.typingNotificationString = text; } } diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index a43b40a18..afcb068b5 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -140,7 +140,7 @@ - + @@ -46,9 +45,4 @@ - - - - - From 435da06a0c5613cceebcb9bf1897068a27464208 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Thu, 25 Mar 2021 10:26:15 +0100 Subject: [PATCH 03/38] Explore typing notifications inspired by web - generated strings updated --- Riot/Generated/Strings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index dd05e41f8..0d5515a5a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1514,7 +1514,7 @@ internal enum VectorL10n { internal static var identityServerSettingsDisconnect: String { return VectorL10n.tr("Vector", "identity_server_settings_disconnect") } - /// Disconnecting from your identity server will mean you won’t be discoverable by other users and be able to invite others by email or phone. + /// Disconnecting from your identity server will mean you won’t be discoverable by other users and be able to invite others by email or phone. internal static var identityServerSettingsDisconnectInfo: String { return VectorL10n.tr("Vector", "identity_server_settings_disconnect_info") } From 630bdde126f2303096c9f6c547c38c68184cf299 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Thu, 25 Mar 2021 11:53:45 +0100 Subject: [PATCH 04/38] Explore typing notifications inspired by web - bug fixing --- Riot/Modules/Room/DataSources/RoomDataSource.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 880f6dc65..210f27c93 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -275,6 +275,8 @@ if (self.currentTypingUsers.count == 0) { + self.typingCellIndex = -1; + // we may have changed the number of bubbles in this block, consider that change return bubbles.count; } From 8c21eb0ff627cdcdf2cb6f161a46e42feed862d2 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Thu, 25 Mar 2021 22:15:18 +0100 Subject: [PATCH 05/38] Switching composer between text mode & action mode --- .../Room/Actions/Contents.json | 6 + .../action_camera.imageset/Contents.json | 26 +++ .../action_camera.imageset/action_camera.png | Bin 0 -> 469 bytes .../action_camera@2x.png | Bin 0 -> 780 bytes .../action_camera@3x.png | Bin 0 -> 1118 bytes .../action_file.imageset/Contents.json | 26 +++ .../action_file.imageset/action_file.png | Bin 0 -> 653 bytes .../action_file.imageset/action_file@2x.png | Bin 0 -> 1269 bytes .../action_file.imageset/action_file@3x.png | Bin 0 -> 1845 bytes .../Contents.json | 26 +++ .../action_media_library.png | Bin 0 -> 519 bytes .../action_media_library@2x.png | Bin 0 -> 851 bytes .../action_media_library@3x.png | Bin 0 -> 1238 bytes .../action_sticker.imageset/Contents.json | 26 +++ .../action_sticker.png | Bin 0 -> 582 bytes .../action_sticker@2x.png | Bin 0 -> 1050 bytes .../action_sticker@3x.png | Bin 0 -> 1457 bytes Riot/Generated/Images.swift | 4 + Riot/Generated/Strings.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 219 ++++++++++-------- .../Views/InputToolbar/RoomActionItem.swift | 30 +++ .../Views/InputToolbar/RoomActionsBar.swift | 129 +++++++++++ .../Views/InputToolbar/RoomInputToolbarView.h | 36 +-- .../Views/InputToolbar/RoomInputToolbarView.m | 135 ++++------- .../InputToolbar/RoomInputToolbarView.xib | 12 + 25 files changed, 467 insertions(+), 210 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@3x.png create mode 100644 Riot/Modules/Room/Views/InputToolbar/RoomActionItem.swift create mode 100644 Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift diff --git a/Riot/Assets/Images.xcassets/Room/Actions/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/Contents.json new file mode 100644 index 000000000..aec39f6e4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/Contents.json @@ -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" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera.png b/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..799e2c02a34e0e95981ca836b05eb11146523f1d GIT binary patch literal 469 zcmV;`0V@89P)(3s!#c*;}R93a_= z+9S8fUHeN2@P&2%!yW{@>MjtJ1F_HFLV=)&o}{Z(jN1%4tYofuiSjlgSjc7TF%VC_tsnc z>X@5tVBBJ(s}ow&U=O19u(xRm?dyQMPIEDN;f#*TqLmp)Nz)=4;_31SN?hDwiUnZ00000 LNkvXXu0mjfO-#xO literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_camera.imageset/action_camera@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..191b388f9587a3ecb24cbb846a02a88fff45975e GIT binary patch literal 780 zcmV+n1M~ceP))kBx-q1IYGk-+DNpBr{oPatpuE)TkR~g#%6kR=(|TLg^E@pjES9fAblZSPQ83J_Jb`_fS0Z*NHss zuSNA%1Pe4QNz!2r?!g7165tac;UuEYt}GpU4T;0VGFrebP_WVn9c+W@}$=_ekOn;B0nMJpmW)We6=aRGoAtoxa1A+{rvd_1(-76yAVBK^t@rmk$`nWTZGvVQ8qJ$; zzkcu%Ez+t$SD!uVl0qLX!UpM^TlWWOiPi+N%PP^PaN;Crmwc?k>!Y+cL<=aigw{od z_Qi3;Yo^#nOSED^&M8~#c?CK0K!soCX)i~^gtL5T{kOdi5mkofs`an6?g}Rmv1Yzu zU!S63YM0o?^Svz<&(}FIpCSG7tu);9gZ$eoi3gL+o-}@{hbQH06P#whe_@50E?r7d>@@pbX^6NK3Cm%ga2u-*(T&N|={#uJ5$G~d z(xA0s^>e`sRKrZgYMU@)zyTd&Z2mY)?7}ciIHa#go31ToI(&9lmX9-K9?p>H9Gm_TgI&f9eH7LBtc+h03?5K;o=MQ9Gu z5MdBx?w$}pN}%IKmew?G|Hei^k_f|w8QtBQW#%_a% zY!6F0>A1J4r9Z5{By+^7|Ir8 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/Contents.json new file mode 100644 index 000000000..63ec74393 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/Contents.json @@ -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" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file.png b/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file.png new file mode 100644 index 0000000000000000000000000000000000000000..0d944b128424028f710a5f0b169375c475bc561d GIT binary patch literal 653 zcmV;80&@L{P)f8zenJf`o&3bDvOKq|zItC!l%a0g8KrniDiPpdx8O zjdzC~Z(JPPsonB~Ixn`@_V@4nJ39;TzlVT3gd6R<3Kjz*(FX!g4^1`b2+`g?_g|m? zlYoYMQYDeT^@{I=fEb1C=S2{15vy>Tk^+DBA`nW_M*2+9PLiu({-4GIdAJ~oExg>A zZw4p)?&9SrCMCAgED(FQ7AV5ey3{s=D_&<}P_V96;9<1i&fwaDlPb9RZntgjXxdnS z;i#nXoZxWY4SaydrSCXHRN_;LS9I7&;4hq9hR?b|-((}toNs$k9-d!#+y3>jR=|lq z^fEXR9c1w9F-702;0u%``$_`N*PU0OXly`gd)^%l^Mb~z(XhDi4FB!Fn-+PZ=%u#c z^cueTHe|RFWPin0^UjoN(J`tt$+D@gs-;zkPpbr0)?? zNOvhghphn=p#|22`+Mv2d+$NZ9%!LqJ{zcsl0bS|Q4(=)U-wf}c%?uV&hc(vABnwX nW%1rf7n>{?U-+XK?oIpweW0!btEzG400000NkvXXu0mjf4U{R% literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4f4522ab7be943bcede71985f74aa89bd17c54af GIT binary patch literal 1269 zcmVqHdC&x}(E>5?k!AUmL}fJEHh*OnDDT|pCx7V*^N3DHzayMo)hf?HmJ;@lOqtbi=R zA_+oVdk*I~b?}Tm_BbEqf&8Va$z{d>q`aTDzj^0+trs=ml|MR=7>)?KRWfz*QP$djwR@A2c`s)`-M>Bog?AOc5~v8< z^EVeyMUy1FfcE|C%Zl));wT=4;aP<&mz!1-PUn> zOh`Pj&EJXvHh3k`dMDGu2nPCyXxZl0V8g2W!?y(AF@|IKdAm5HETL+(r$~kAp{8x_ zMm@mrprE6VBIWe(3s-nn(C!7Ub2s9FhCgaVKm{rnKB9v3-2^HquGA?B1gr6D8^G`? z3VjN^1O1cL;8{ZGHv+|$3`pQ{gYfl-s}B9svJPb~Yj3o23=n+N75>#vR|jPkl@+%e zAV=o)iZa(q1~5Eji443zd`iQ!hw8r$eQF6F_f+`%{=*MlD1wjM4Q!;x`Fc>G$_6Cx zV5ByNm)@Tmd>k98&Qqa0%DX1jjkCO7Nh#=+EyGF%l)(q>evq2?nDLN+31s29=-ogL z{QU6z21?{?fDN7-upIbYBdKxgK^DHB`7!F1T;SvDx=bp9pT0itl+80eQ~)0ke6Ofi zs%*fz@Z2`ny!qEU{BT66(G2KH?M9-~I*<#c)L?e_Z68YD*~nSSNYA{--aquA6sNl6I|deDRr94N_t?6W*Gkc8@=d*gRDzP7%t~sQtHE# zb>IU7J`>nNXr3*Sb33iM6wO>O?vvLg6}y|N*o_Ej87lLX4kw`(IaOg+Q7fUXLH@zz z8>6T3yoE=2CskDR((q=Zm%vvIY-a<)FkCJvIyLy9^Fg=%Fhn}f$*IJ6Yu1BJW+l4; zs6OZes+GK=cwiToW{I4Xn%|`w`-m#C8z973dn@HxvWz$$_~4cA8Wh1-2<%nsfz<%Y z#T=OFyWCJJTvld2HTHP76rX1<4GEGn&sDQhRTXxldFPaL%-8>;eK7z>S+f#|dr!_z z*MX0(0>wYxprZ9?7A;pJc&kX>=$q|^+32mK()|FRDDXKw7;OV-LK)^qI<&dxBFAyT z26MKOHT)8l>}J3>49{}fkuBP4!N(oW6*5m|m%0f#KOb0l0^Xa;$x#7q(g&{l+$yT{ zfa7_IbSbdUz|9-{Jm9ugo8PP(tTwImgr?ztPnpGQQOZ1GkE9yEApS!fD^EV{!R+0( fz<1hdr-l9j#u{U2?FBcK00000NkvXXu0mjf6f$98 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_file.imageset/action_file@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..13ed744fc0e4e92f638233252e53fda390e53e23 GIT binary patch literal 1845 zcmV-52g>+~P)x*`WdL>haq^MF5Bw?6vTre2#FC8zLA2jT@#Xx!;t$sMTmM3 z1U>1#SHwWaftcIqGY~tU7q|8y2WFa}#GD}70|6^g5RA*t=l13fIAUfAN=zz_x{^T? zN)APj*hQINDkZ@%E z6ypcDI)R9pT0vuS)SnI07%AZ_@}kwt_Ai0?I&al=Jarw`&|X7`oiDGpIzYh85JVvd zSI7CwjTInW&q`9?6{1N2Oyx* z6+|azvvhKBZe>R;!|6k6bgOIyWs@TV!C_fP&ZFPoHfrB^N^h2Z7inUQdUy9g&Annc z-4A(0)`Ti4R-bft_muQtDGg*0^0=+5$kW6sC)?x9Z&Tkml4DaKUGuBbSJ3e7*(6dr zImu`V%_zP?vO8cwlJCgqSdr5*k76gT$!DnqWxYLDa>ihcS;e!lJPTHlO$JI-TLnjy zLJ;lkC9>VQCzrt^)B4wR-gJY-{Z+P(R0TN*rxi4MNG*zxV~aKva!`S+f*b_d2h~5m zTLI%k>Qnfl(T1P~l&(OgL0>_P>4m2x;)LtzqZ1&fUxfmh2FV9meGg|V=k1@5G~=_I zfd;dfrjC>b$;4t2w)9-e;Jm$YbbtJG$Vt)w8SN!=E2kgw+IrD_iZm0GrZz1*OCPcO zDmytNp{oPt8&T#QJvWkrX#ggm66a@2HT9xgwz}v*BZ$Ly+T+Z-ul5;zRW>sX+y6^nYIPSS zyf+_V6Yu78nfnlJr3WR_;eN$3awMvW1)zycVHo1EYcs1sEr=n)2TGgd0!9@7^j*@d zi$(kzEEV~v^jOPFPLc*JmXjlq2vU9Q9s}06cY?_l+#(2DGxfiAdj_qip~Uw$<11Pw zE0kTAoj?-vAka4YxyOG>9}|&Whn%qOL!>k!O*v8#P!mOcBv30Q?T0Y32>@r3Z2J&t z38X&K6$Oc)WHTfdhZ+bGZKsb77B}{llO(ryt4bit06B`O<*(hruDr<_s(tC>iquZB z^Ibgv7_SLuIA&JKL68;2;l^y=q$aAarT^U2KY#qkyC&553l_yslSw&K<1bE3?HJk@ zS*j-o;Uu}%Pj?SZ8nDi&oE7cS>F1lqhp)1f-izJX$ zz_~A0KYhQX?lNopn$uSykr?K|w!K$2d2*6DAqUh0byQKhojp>X0Ouzn6#vv}wP5U{ zk_}Qd-eewoCQ)m$6En=rOX`%mB6sWA=8sCpl*mEkE>`h|U2>hgz#M5>(*X&hrKWhshC4wTw@wF&kUmo~ituogoHG$p$Z@|AXv$nOT6p4yG=Sf`@Aq-~pfkdI%ZE;nZ?_-vlUl7X^Ay02qk&wAvX+ zn=hRdya@#Xfp?cSSNtXv2O~@1*FZ#Hq*x*Aik49pXAkr-Ol-HcuqOvk_a@`eT_>uxQz@IUCV=5PpI2hL zGg>Fks?MN9dy)hC0x*8FZo@osM0+J(u28E!f~LTah^%%L+fI{)Vs9J1c9|wM$6Iu9 z2)huFn4PG7}pW~efNXU1I&m1c+WvW0ZYx=V*H6T;A7;56nKThS8d{B<(Xb6dA;6#2oB{kBd;n=4o!Mxh`y{Dtr=>y}MZZ+p{yXvj97GJZ@NA=ksrub;j`&7^?$94T{C0-*CYz zy}919q6S3@crm!@qCf*m0KG)xR`2xI_N{9y)8sv(M1%xX2On#Wuzx`bK>_;oxIOf@ z@!%lGu?;9G?s^Qs!=vC8looO(0r;9xH7GB&U;~H$oC+ue5r&Lme|baZ!H~?C)lxh{ zp#TTZrY9ZK*J4%&7vur8K^hTCObCVOP5zy{%UFMJpwIVv9OK3Ct98Ex@iN@iY6BbG67?3 z`Ovgv5+MEf^!S}2ujk7EG)xA)S@V1sNFYEonwgD$Q7oOE0w@V!wa`_!AxoC1-|kaBC)2NF<(}bVeGj zA^N04$eF@Y3D?!WZtGMU=|PSZo(oBnqV40Gyi7_-GB;_Qe|AkO2NBAu5_i%2fhMKA ztm2v!pxg&%ir%HD3(=P_vqtaI6t=wRoi-sXRb@^_Z(l-~h;;%i?}E`T2^6qO2oJ(0 z6*jT302DXgB~=GbpMCwHM1(XcKpyxKY#1CDO6Cx`N!vSb!(HVX`$2nGlX`BTK;#}7 z?&smM;P&Pd8VO~P#x^s^Ecmyvej*R~$|NFc@{-tNR<~O0RE#WT?;M~tfK(@C{ouLS djvYJh`3o?@GrUq}Bh>%^002ovPDHLkV1l}-Yz_bb literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_media_library.imageset/action_media_library@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2dfd074ab0b731e1eb15eb1e57d0eac22426c876 GIT binary patch literal 1238 zcmV;{1S$K8P)f8deH)w|dOjr4mx=dw;8{L8U+l@D`p3oUyWUO!*Wt8=eY`y;Kcx^)v7ao({HZ zAW|Ikl*t7Op#Wy%0%D8utSj;fv{kd0A_j7Qb2ORccOmxxRpu9)O`}7YK#e>Q-u1*J-$;-+C*x29v3VMyEh7 z4De%b<#S`)V7Ic-viQI;O(abMVMmrAx^L=-L9_f)gfux!o@|G87O4%$&)^5~xo;gI?qpq=ZZv zhM_>>N}w)c33RKWLKns`6j;rQ$z?X1&r0;2>2k*?`j%a)@JM979wbffda1VfSSDvo z!OS-~z>hHH=-a4@B02BFX-D72m0U;B_p}mTBm0A7^nEYkO%?AwdFl9{!-Con+b!6W*mDkr=9z;eg95|95jS3yi!THQeQ1sG@Ir*DKYtYJmhh z8}m=)s8<=Y0=Wg*?E%G~O~x%nqHA$wCkPAp6kb!expUm_3%jd z=E;T|jGO786`(x$q_e%yYC!Z43*6md-At(&YFfyk(WRyHDd?v2ePVr@LL!Ail=oSg z{Rp=bCyk0bvI!k6el`caqa?@WIXO8wIXUL=54DWl7jMf|&;S4c07*qoM6N<$g3o0> A;Q#;t literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/Contents.json new file mode 100644 index 000000000..adf12f0d6 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/Contents.json @@ -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" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker.png b/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7a04d3fedd7a64764e1531e27487662a2119e5 GIT binary patch literal 582 zcmV-M0=fN(P)dYxl00==P#{sCeR7<~H>LYJ9)pj5k`ubh3! zVCm|h1n4vY*n{a9BMc@PskGp7=kOR%oo5gid?0QucN(>{Nlc0#)tESdFJQz@9{%r? z$71g+T{kvOs0s7HwB9dN+i%0M6k!eK1yxguBVq8e`j;x16EC8ulrUg`HTWhJ!_dGT zMj8F-U6Bqm+?fA+=?zk91vULOGAjuKb3+b3foZf;*Le)Wd=O5%&xOwug68sIb+;vh z#mjx)SzK6?mD`0n>dpb@BFvY=af2RYIb^1-{%W-?m7+;YrkQjb(zm#fxs0AssN6CW{HaNQP2ekUN+ z@|54}$?8qDZDbK2F|dcixCIj($nG$T;p)-tl|j+VNj@S zGeI3@kVQPzz?u|y0wZD4WxiUkAh9o3V%5zmo(Xvg{<&(Lo8O6yy-twjBlL@S21JM0 Uvtx15!TpF literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..13a2b75119e96e2c5819dbaa3eea71b33b314557 GIT binary patch literal 1050 zcmV+#1m*jQP)~;JRlSFEMQUp$ROgiAY{a=pGPH^;4?Q*#R88DOq`T9v-L2Hwg*m@Eh8(Y5`Eg|=t*0v2PNoBx;aCeUs zY(X}hrZpt70dw_Z?Qy82@OTt92&~*#JGCT}5*aX8KRU_K>qfMo>h)rIJHhy9IAlC> zzXCxCMgSsQ2$Ys%Z97q81;jvkZhchZ86qgQ9TtN~1KMGOP=ryV9X5A%bP_deU#fA?Ua-cuI#3Kmo>!lM`}2Y(IpCkmtD-7{7QXuavtN_MveF zI^Ms62|};JYtt$iSD@qliNba7jpvU!gb?1sgn>eAn%)l>z*!|sM(q2^ceM3kSOHan z357rf&ZD3K0vBL>@FvxD12p&r4=HySfmrfSkb$otEM!;aMB?Zor^dNY>)vZL-+#1C zgI@kPEQ%Zy34_iJNS!@9`mO(DohthvTM1Ep0k<{BszfGigOG;zo>ja~$M>cDQ@t5d z_#+ZV274V=?-oB?3>r1_{h-kOe`dysN~uQ_LS(muXEm*0jCbp5(i@a?Gayjup#w-4 zEp#_b^Gz$j7H4RHS^Q&Nty7k*CqI&Dex5va-dr|s@PGjXbb){c$OsM_zj!{7Jmqo0 z66q*XYX5e5w-WZb2u{Fr$Oyjqoz)-nv-RUO6i|=T6o?Yf1PsuNIdGH3hiw1NXMcTK z0Y#^=X2%fE1PthO%!Yr&2~p|2)Stt@tRFs`cqXzlce}K+ zr8i&$VxoLuhb|fqtSrPcv3jF9l&f5HKt^OGo{41u?}zJm$EuKlV0*^TQ zF?q98Ue5`4UgzI6|BAS-+xO&Af>xhHJd^3qtnHqX?i^4~H14dOA@ivzQ>Mh>A1bBE UH10O~g#Z8m07*qoM6N<$f+`HqF#rGn literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_sticker.imageset/action_sticker@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d000e8f4c8ab95168d1c282536f38a7922870313 GIT binary patch literal 1457 zcmV;i1y1^jP)Sy0`|8_!v@R{{KgK? zdxK5LNG3sn9OVxQVHsu+L^!=cXEjU6r4|(A8y?R5-!{xPF6X6gT1lw{)kw|+*S4IM zG(~2rB+X%U4E2#Cv^51EXkrozS{Uvf%xcbwg9w%vPWBEUA&Ek4{^a*H{*hNuV-WP_ zd2j2`{Cq=dn0KP^IBBn#CT-&oQ_A0g`pXeob5iXaVl;y0-|zPU=@{-2T-bI^Cb?!K z2q|%Ln{|T8v;k8E$;=$?M`+!K=Zv4;TP#xrnVPO6Chsja2YtG7eSmumSJ>ajRoJm3 zXzcraxaau(P3$T}tRTyVr*O|f+xj8)@u&xa?t}LKfp37gFpC^PzVFv=zLge56gi4w zLQHqy8$qk_!>Grc$e*%s3F?JML?izCx2z-rS@Qj@u;C_UOWUxBO0y7L;`2Ik+=_Xx zk%HvOxR{!CT%Iz#=C7l)y)h{k2{1C9L?-F^lRc?_+AxFg+RlhSyh0m>6P%dG+Aw33 zDQ1EL^}y2_cCVfcj2F7{q6UJ9yArB<-FxXm1c+vzw#|USB*OtW7&jq>Mp5vxxeM+umsRr)_wM96|gkcv$-O zs?V=Ix|||svgb%?wP69ZAn9vpEy;fBZjk6Y2(;|#s$jZ7u7p+G8T0Rd>$4bMSLRwR zMX~l#&eo2}xsg*1P1Y%tk0%M)i^)0CmjkqTeLVT75t7IeB1e#g=t9Y*v|P%gu2w+; zCjXc#=MJPKsT?741lhJdf>I$juFerTS@s^Kkt0M33f4!EE7y`BjnAVV3&N@bIYR7; zA}RP2N+7suMbstI?#{9~D~lWU3<&^e3zY04nO#{ACrS7U^l_SIoDksdyjT${! z+TD4`5vKOVWFImV2!~f_u@;OTgjAa`^O7S>6%>5B1&$)lFGpd=l^%B>)$`>iM=6)H!<3lr{QD?*N7yg$fBU+R>6@3bKuT1F%d zucG7##)7==OIJg_p+=TWXF`sX`RFN9j*w^*=T&cT$hqMU$O+Wpg3z$#RJ0r+@h;cP z$G>a@eWx6_+qe3BT*K6vdKq$rRQseYbh=XDK}M8Pp@R4Wh!s?p93hn;sjzKcx5-0N zQp%Jgq`ncurrentAlert = 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]; + } +} + +- (void)roomInputToolbarViewDidTapFileUpload +{ + MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new]; + documentPickerPresenter.delegate = self; + + NSArray *allowedUTIs = @[MXKUTI.data]; + [documentPickerPresenter presentDocumentPickerWith:allowedUTIs from:self animated:YES completion:nil]; + + self.documentPickerPresenter = documentPickerPresenter; +} + #pragma mark - Dialpad - (void)openDialpad @@ -3321,80 +3445,6 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo 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 @@ -3641,27 +3691,6 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo } } -- (void)roomInputToolbarViewDidTapFileUpload:(MXKRoomInputToolbarView *)toolbarView -{ - MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new]; - documentPickerPresenter.delegate = self; - - NSArray *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]; diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomActionItem.swift b/Riot/Modules/Room/Views/InputToolbar/RoomActionItem.swift new file mode 100644 index 000000000..e22f9bda5 --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/RoomActionItem.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +@objcMembers +@objc class RoomActionItem: NSObject { + var image: UIImage! + var action: (() -> Void)! + + init(image: UIImage, andAction action: @escaping () -> Void) { + super.init() + + self.image = image + self.action = action + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift b/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift new file mode 100644 index 000000000..7f4bdcb98 --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift @@ -0,0 +1,129 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +@objcMembers +@objc class RoomActionsBar: UIScrollView { + // MARK: - Properties + + var itemSpacing: CGFloat = 20 { + didSet { + self.setNeedsLayout() + } + } + + var actionItems: Array = [] { + didSet { + var actionButtons: Array = [] + for (index, item) in actionItems.enumerated() { + let button = UIButton(type: .custom) + button.setImage(item.image, for: .normal) + button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside) + button.tintColor = ThemeService.shared().theme.tintColor + button.tag = index + actionButtons.append(button) + addSubview(button) + } + self.actionButtons = actionButtons + self.lastBounds = .zero + self.setNeedsLayout() + } + } + + private var actionButtons: Array = [] { + willSet { + for button in actionButtons { + button.removeFromSuperview() + } + } + } + + private var lastBounds = CGRect.zero + + // MARK: - Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + self.showsHorizontalScrollIndicator = false + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + self.showsHorizontalScrollIndicator = false + } + + override func layoutSubviews() { + super.layoutSubviews() + + guard lastBounds != self.bounds else { + return + } + + lastBounds = self.bounds + + var currentX: CGFloat = 0 + for button in actionButtons { + button.frame = CGRect(x: currentX, y: 0, width: self.bounds.height, height: self.bounds.height) + currentX = button.frame.maxX + itemSpacing + } + + self.contentSize = CGSize(width: currentX - itemSpacing, height: self.bounds.height) + } + + // MARK: - Business methods + + func customizeViewRendering() { + for button in actionButtons { + button.tintColor = ThemeService.shared().theme.tintColor + } + } + + func animate(showIn: Bool, completion: ((Bool) -> Void)? = nil) { + if showIn { + for button in actionButtons { + button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height) + } + for (index, button) in actionButtons.enumerated() { + UIView.animate(withDuration: 0.32, delay: 0.05 * Double(index), usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut) { + button.transform = CGAffineTransform.identity + } completion: { (finished) in + completion?(finished) + } + } + } else { + for (index, button) in actionButtons.enumerated() { + UIView.animate(withDuration: 0.2, delay: 0.05 * Double(index), options: .curveEaseInOut) { + button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height) + } completion: { (finished) in + completion?(finished) + } + } + } + } + + // MARK: - Private methods + + @objc private func buttonAction(_ sender: UIButton) { + if let action = actionItems[sender.tag].action { + action() + } + } + + private func setupView() { + self.showsHorizontalScrollIndicator = false + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index e5559e9fe..b2ad883b8 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -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 -/** - 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 diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index dfceba978..cb3a55146 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -120,6 +120,7 @@ const double RoomInputToolbarViewContextBarHeight = 30; self.inputContextImageView.tintColor = ThemeService.shared.theme.textSecondaryColor; self.inputContextLabel.textColor = ThemeService.shared.theme.textSecondaryColor; self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor; + [self.actionsBar customizeViewRendering]; } #pragma mark - @@ -142,6 +143,7 @@ const double RoomInputToolbarViewContextBarHeight = 30; RoomInputToolbarViewSendMode previousMode = _sendMode; _sendMode = sendMode; + self.actionMenuOpened = NO; [self updatePlaceholder]; [self updateToolbarButtonLabelWithPreviousMode: previousMode]; } @@ -329,92 +331,7 @@ const double RoomInputToolbarViewContextBarHeight = 30; { if (button == self.attachMediaButton) { - // Check whether media attachment is supported - if ([self.delegate respondsToSelector:@selector(roomInputToolbarView:presentViewController:)]) - { - // Ask the user the kind of the call: voice or video? - actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - - __weak typeof(self) weakSelf = self; - - [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_camera", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->actionSheet = nil; - - [self.delegate roomInputToolbarViewDidTapCamera:self]; - } - }]]; - - - [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_photo_or_video", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->actionSheet = nil; - - [self.delegate roomInputToolbarViewDidTapMediaLibrary:self]; - } - - }]]; - - if (BuildSettings.allowSendingStickers) - { - [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_sticker", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->actionSheet = nil; - - [self.delegate roomInputToolbarViewPresentStickerPicker:self]; - } - - }]]; - } - - [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_file", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->actionSheet = nil; - - [self.delegate roomInputToolbarViewDidTapFileUpload:self]; - } - }]]; - - [actionSheet addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] - style:UIAlertActionStyleCancel - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->actionSheet = nil; - } - - }]]; - - [actionSheet popoverPresentationController].sourceView = self.attachMediaButton; - [actionSheet popoverPresentationController].sourceRect = self.attachMediaButton.bounds; - [self.window.rootViewController presentViewController:actionSheet animated:YES completion:nil]; - } - else - { - NSLog(@"[RoomInputToolbarView] Attach media is not supported"); - } + self.actionMenuOpened = !self.isActionMenuOpened; } [super onTouchUpInside:button]; @@ -443,9 +360,55 @@ const double RoomInputToolbarViewContextBarHeight = 30; self.rightInputToolbarButton.alpha = 0; self.messageComposerContainerTrailingConstraint.constant = 12; } + [self layoutIfNeeded]; } +#pragma mark - properties + +- (void)setActionMenuOpened:(BOOL)actionMenuOpened +{ + if (_actionMenuOpened != actionMenuOpened) + { + _actionMenuOpened = actionMenuOpened; + + if (_actionMenuOpened) { + self.actionsBar.hidden = NO; + [self.actionsBar animateWithShowIn:_actionMenuOpened completion:nil]; + } + else + { + [self.actionsBar animateWithShowIn:_actionMenuOpened completion:^(BOOL finished) { + self.actionsBar.hidden = YES; + }]; + } + + [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:0.45 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseIn animations:^{ + self.attachMediaButton.transform = actionMenuOpened ? CGAffineTransformMakeRotation(M_PI * 3 / 4) : CGAffineTransformIdentity; + } completion:^(BOOL finished) { + }]; + + [UIView animateWithDuration:.2 delay:_actionMenuOpened ? 0 : .1 options:UIViewAnimationOptionCurveEaseIn animations:^{ + self->messageComposerContainer.alpha = actionMenuOpened ? 0 : 1; + self.rightInputToolbarButton.alpha = self->growingTextView.text.length == 0 || actionMenuOpened ? 0 : 1; + } completion:^(BOOL finished) { + }]; + + [UIView animateWithDuration:.3 animations:^{ + if (actionMenuOpened) + { + self.mainToolbarHeightConstraint.constant = self.mainToolbarMinHeightConstraint.constant; + } + else + { + [self->growingTextView refreshHeight]; + } + [self layoutIfNeeded]; + [self.delegate roomInputToolbarView:self heightDidChanged:self.mainToolbarHeightConstraint.constant completion:nil]; + }]; + } +} + #pragma mark - Clipboard - Handle image/data paste from general pasteboard - (void)paste:(id)sender diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index fbc52e9f1..e99b0ef66 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -27,6 +27,14 @@ + @@ -105,11 +113,14 @@ + + + @@ -125,6 +136,7 @@ + From ddcce2e49135cbc11b9f5deae6d9f64017a15fd3 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Fri, 26 Mar 2021 11:18:05 +0100 Subject: [PATCH 06/38] Switching composer between text mode & action mode - Deselect text if user taps action button to avoid overlaps --- Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index cb3a55146..a0b5aceb8 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -372,6 +372,8 @@ const double RoomInputToolbarViewContextBarHeight = 30; { _actionMenuOpened = actionMenuOpened; + self->growingTextView.internalTextView.selectedTextRange = nil; + if (_actionMenuOpened) { self.actionsBar.hidden = NO; [self.actionsBar animateWithShowIn:_actionMenuOpened completion:nil]; From f08434b684e7165b821cfb664878d335d0884fda Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Sat, 27 Mar 2021 07:51:51 +0100 Subject: [PATCH 07/38] Switching composer between text mode & action mode - Fixed: if you start typing while the new attachment sending mode is on, the send button appears --- .../Room/Views/InputToolbar/RoomInputToolbarView.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index a0b5aceb8..5997ce8bb 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -350,6 +350,8 @@ const double RoomInputToolbarViewContextBarHeight = 30; - (void)updateSendButtonWithMessage:(NSString *)textMessage { + self.actionMenuOpened = NO; + if (textMessage.length) { self.rightInputToolbarButton.alpha = 1; @@ -372,7 +374,13 @@ const double RoomInputToolbarViewContextBarHeight = 30; { _actionMenuOpened = actionMenuOpened; - self->growingTextView.internalTextView.selectedTextRange = nil; + 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; From e6663a9148707ddd84aaab01080dda91cabc6bfb Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 29 Mar 2021 22:26:03 +0200 Subject: [PATCH 08/38] Explore typing notifications inspired by web - prevent timeline from going up and down by keeping the space allocated for the typing notification --- Riot/Modules/Room/DataSources/RoomDataSource.m | 4 ++-- Riot/Modules/Room/RoomViewController.m | 17 +++++++++++++++-- .../BubbleCells/RoomTypingBubbleCell.swift | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 210f27c93..8cef6f9b2 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -273,7 +273,7 @@ [self updateStatusInfo]; } - if (self.currentTypingUsers.count == 0) + if (!self.currentTypingUsers) { self.typingCellIndex = -1; @@ -285,7 +285,7 @@ return bubbles.count + 1; } - if (self.currentTypingUsers.count == 0) + if (!self.currentTypingUsers) { self.typingCellIndex = -1; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index abac470da..6e5173a6f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4109,9 +4109,22 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo if (needsUpdate) { + BOOL needsReload = roomDataSource.currentTypingUsers == nil; roomDataSource.currentTypingUsers = typingUsers; - [self.bubblesTableView reloadData]; - if (self.isScrollToBottomHidden) + if (needsReload) + { + [self.bubblesTableView reloadData]; + } + else + { + NSInteger count = [self.roomDataSource tableView: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.roomDataSource tableView:self.bubblesTableView numberOfRowsInSection:0]; if (count) diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift index b69541f88..0fdbc7a8e 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift @@ -51,6 +51,8 @@ class RoomTypingBubbleCell: UITableViewCell { override func layoutSubviews() { super.layoutSubviews() + self.dotsView?.isHidden = userPictureViews.count == 0 + guard userPictureViews.count > 0 else { return } From c1caaaa775773cc1e27f8826e641a26c9abb82f2 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 29 Mar 2021 23:45:47 +0200 Subject: [PATCH 09/38] Switching composer between text mode & action mode - Fixed: The final frames of the appearance animation of the new composer buttons are missing --- Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift b/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift index 7f4bdcb98..0cf11e292 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift +++ b/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift @@ -77,6 +77,7 @@ import UIKit 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 } @@ -98,7 +99,7 @@ import UIKit button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height) } for (index, button) in actionButtons.enumerated() { - UIView.animate(withDuration: 0.32, delay: 0.05 * Double(index), usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut) { + UIView.animate(withDuration: 0.38, delay: 0.05 * Double(index), usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut) { button.transform = CGAffineTransform.identity } completion: { (finished) in completion?(finished) @@ -106,7 +107,7 @@ import UIKit } } else { for (index, button) in actionButtons.enumerated() { - UIView.animate(withDuration: 0.2, delay: 0.05 * Double(index), options: .curveEaseInOut) { + UIView.animate(withDuration: 0.3, delay: 0.05 * Double(index), options: .curveEaseInOut) { button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height) } completion: { (finished) in completion?(finished) From 8a0674404e15b1a0d6135feedd72cd88c5f601f5 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Sat, 3 Apr 2021 00:03:57 +0200 Subject: [PATCH 10/38] Switching composer between text mode & action mode - Tweaked animation speed --- Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift | 8 +++++--- .../Room/Views/InputToolbar/RoomInputToolbarView.m | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift b/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift index 0cf11e292..3f78b9cb3 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift +++ b/Riot/Modules/Room/Views/InputToolbar/RoomActionsBar.swift @@ -99,7 +99,7 @@ import UIKit button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height) } for (index, button) in actionButtons.enumerated() { - UIView.animate(withDuration: 0.38, delay: 0.05 * Double(index), usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut) { + 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) @@ -107,10 +107,12 @@ import UIKit } } else { for (index, button) in actionButtons.enumerated() { - UIView.animate(withDuration: 0.3, delay: 0.05 * Double(index), options: .curveEaseInOut) { + UIView.animate(withDuration: 0.25, delay: 0.05 * Double(index), options: .curveEaseInOut) { button.transform = CGAffineTransform(translationX: 0, y: self.bounds.height) } completion: { (finished) in - completion?(finished) + if index == self.actionButtons.count - 1 { + completion?(finished) + } } } } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 5997ce8bb..c76bbd038 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -201,7 +201,7 @@ const double RoomInputToolbarViewContextBarHeight = 30; if (self.mainToolbarHeightConstraint.constant != updatedHeight) { - [UIView animateWithDuration:.3 animations:^{ + [UIView animateWithDuration:.15 animations:^{ self.mainToolbarHeightConstraint.constant = updatedHeight; [self layoutIfNeeded]; From bfd605dff72f21c4f1489a65229ab58204e08cf6 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Apr 2021 13:09:30 +0200 Subject: [PATCH 11/38] Limit typing notifications timeline jumps --- Riot/Modules/Room/DataSources/RoomDataSource.h | 2 ++ Riot/Modules/Room/DataSources/RoomDataSource.m | 4 ++++ Riot/Modules/Room/RoomViewController.m | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index ced9e687a..d4ec45c91 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -98,6 +98,8 @@ success:(void(^)(void))success failure:(void(^)(NSError*))failure; +- (void)resetTypingNotification; + @end @protocol RoomDataSourceDelegate diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 8cef6f9b2..2122a398d 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -950,6 +950,10 @@ }]; } +- (void)resetTypingNotification { + self.currentTypingUsers = nil; +} + #pragma - Accessibility - (void)setupAccessibilityForCell:(MXKRoomBubbleTableViewCell *)cell withCellData:(RoomBubbleCellData*)cellData diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 6e5173a6f..9ac281f7d 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -511,6 +511,12 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo [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]; @@ -1338,6 +1344,17 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo _scrollToBottomHidden = scrollToBottomHidden; } + if (!_scrollToBottomHidden && [self.roomDataSource isKindOfClass:RoomDataSource.class]) + { + RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource; + if (roomDataSource.currentTypingUsers && !roomDataSource.currentTypingUsers.count) + { + [roomDataSource resetTypingNotification]; + NSInteger count = [roomDataSource tableView:self.bubblesTableView numberOfRowsInSection:0]; + [self.bubblesTableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:count inSection:0]] withRowAnimation:UITableViewRowAnimationNone]; + } + } + [UIView animateWithDuration:.2 animations:^{ self.scrollToBottomBadgeLabel.alpha = (scrollToBottomHidden || !self.scrollToBottomBadgeLabel.text) ? 0 : 1; self.scrollToBottomButton.alpha = scrollToBottomHidden ? 0 : 1; From 18e2666b84942f3e5ddf5cda09f5b8e37b482f7a Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Apr 2021 14:32:25 +0200 Subject: [PATCH 12/38] Consider displaying names in typing notifications --- Riot/Assets/en.lproj/Vector.strings | 2 ++ Riot/Generated/Strings.swift | 4 +++ .../BubbleCells/RoomTypingBubbleCell.swift | 26 +++++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 40ab6cd46..6b58da5c1 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -406,6 +406,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."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 0d5515a5a..ab9947434 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2938,6 +2938,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) diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift index 0fdbc7a8e..ae06ed057 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift @@ -32,10 +32,17 @@ class RoomTypingBubbleCell: UITableViewCell { // MARK: - members private var userPictureViews: Array = Array() - private var typingUsers: Array = Array() // MARK: - Lifecycle + override func awakeFromNib() { + super.awakeFromNib() + + additionalUsersLabel?.textColor = ThemeService.shared().theme.textSecondaryColor + dotsView?.highlightedDotColor = ThemeService.shared().theme.textTertiaryColor + dotsView?.dotColor = ThemeService.shared().theme.textSecondaryColor + } + override func prepareForReuse() { super.prepareForReuse() @@ -97,7 +104,22 @@ class RoomTypingBubbleCell: UITableViewCell { self.contentView.addSubview(pictureView) } - additionalUsersLabel?.text = typingUsers.count <= 4 ? nil : "+\(typingUsers.count - 4)" + 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) -> String? { + guard let firstUser = typingUsers.first else { + return nil + } + + return firstUser.displayname.isEmptyOrNil ? firstUser.userId : firstUser.displayname + } } From 62cb90ffd76a400e349aae61dfef0105e6665147 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 7 Apr 2021 09:32:57 +0200 Subject: [PATCH 13/38] Switching composer between text mode & action mode - Tweaked animation speed --- Riot/Modules/Room/RoomViewController.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 87f9f09a8..99d67c067 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -286,6 +286,8 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo { [super finalizeInit]; + self.resizeComposerAnimationDuration = 0.15; + // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; From f239790362594400277b9d29fa33b22fbb035b70 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 7 Apr 2021 10:28:35 +0200 Subject: [PATCH 14/38] Switching composer between text mode & action mode - Tweaked animation speed --- Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index c76bbd038..aef222b14 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -393,7 +393,7 @@ const double RoomInputToolbarViewContextBarHeight = 30; }]; } - [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:0.45 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseIn animations:^{ + [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:0.45 initialSpringVelocity:7 options:UIViewAnimationOptionCurveEaseIn animations:^{ self.attachMediaButton.transform = actionMenuOpened ? CGAffineTransformMakeRotation(M_PI * 3 / 4) : CGAffineTransformIdentity; } completion:^(BOOL finished) { }]; From 890157dcc8103b80549dcdbae6a8d29635630b2f Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 7 Apr 2021 11:14:08 +0200 Subject: [PATCH 15/38] Crash in [RoomViewController setupActions] --- Riot/Modules/Room/RoomViewController.m | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 99d67c067..6a14a78c2 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1702,6 +1702,10 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo } - (void)setupActions { + if (![self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + return; + } + RoomInputToolbarView *roomInputView = ((RoomInputToolbarView *) self.inputToolbarView); __weak typeof(self) weakSelf = self; roomInputView.actionsBar.actionItems = @[ @@ -1709,7 +1713,9 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo if (weakSelf) { typeof(self) self = weakSelf; - roomInputView.actionMenuOpened = NO; + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } [self showCameraControllerAnimated:YES]; } }], @@ -1717,7 +1723,9 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo if (weakSelf) { typeof(self) self = weakSelf; - roomInputView.actionMenuOpened = NO; + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } [self showMediaPickerAnimated:YES]; } }], @@ -1725,7 +1733,9 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo if (weakSelf) { typeof(self) self = weakSelf; - roomInputView.actionMenuOpened = NO; + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } [self roomInputToolbarViewPresentStickerPicker]; } }], @@ -1733,7 +1743,9 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo if (weakSelf) { typeof(self) self = weakSelf; - roomInputView.actionMenuOpened = NO; + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } [self roomInputToolbarViewDidTapFileUpload]; } }], From a09550a1ee9af09e32e39f0a9354a7175ecd28cd Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 7 Apr 2021 12:26:51 +0200 Subject: [PATCH 16/38] Crash in [RoomViewController refreshTypingNotification] --- .../Modules/Room/DataSources/TypingUserInfo.h | 34 ++++++++++++++ .../Modules/Room/DataSources/TypingUserInfo.m | 46 +++++++++++++++++++ Riot/Modules/Room/RoomViewController.m | 15 +++++- .../BubbleCells/RoomTypingBubbleCell.swift | 8 ++-- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 Riot/Modules/Room/DataSources/TypingUserInfo.h create mode 100644 Riot/Modules/Room/DataSources/TypingUserInfo.m diff --git a/Riot/Modules/Room/DataSources/TypingUserInfo.h b/Riot/Modules/Room/DataSources/TypingUserInfo.h new file mode 100644 index 000000000..54914dc6e --- /dev/null +++ b/Riot/Modules/Room/DataSources/TypingUserInfo.h @@ -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 +#import + +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 diff --git a/Riot/Modules/Room/DataSources/TypingUserInfo.m b/Riot/Modules/Room/DataSources/TypingUserInfo.m new file mode 100644 index 000000000..fa59cde01 --- /dev/null +++ b/Riot/Modules/Room/DataSources/TypingUserInfo.m @@ -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 diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e3379981e..e7c07ec93 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -124,6 +124,8 @@ #import "SettingsViewController.h" #import "SecurityViewController.h" +#import "TypingUserInfo.h" + #import "Riot-Swift.h" NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; @@ -4134,8 +4136,17 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNo for (NSUInteger i = 0 ; i < currentTypingUsers.count ; i++) { NSString *userId = currentTypingUsers[i]; MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:userId]; - [typingUsers addObject:member]; - needsUpdate = needsUpdate || member.userId != ((MXRoomMember *) roomDataSource.currentTypingUsers[i]).userId; + TypingUserInfo *userInfo; + if (member) + { + userInfo = [[TypingUserInfo alloc] initWithMember: member]; + } + else + { + userInfo = [[TypingUserInfo alloc] initWithUserId: userId]; + } + [typingUsers addObject:userInfo]; + needsUpdate = needsUpdate || userInfo.userId != ((MXRoomMember *) roomDataSource.currentTypingUsers[i]).userId; } if (needsUpdate) diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift index ae06ed057..551dca621 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/RoomTypingBubbleCell.swift @@ -82,7 +82,7 @@ class RoomTypingBubbleCell: UITableViewCell { // MARK: - Business methods - func updateTypingUsers(_ typingUsers: Array, mediaManager: MXMediaManager) { + func updateTypingUsers(_ typingUsers: Array, mediaManager: MXMediaManager) { for pictureView in userPictureViews { pictureView.removeFromSuperview() } @@ -97,7 +97,7 @@ class RoomTypingBubbleCell: UITableViewCell { pictureView.layer.masksToBounds = true pictureView.layer.cornerRadius = pictureView.bounds.midX - let defaultavatarImage = AvatarGenerator.generateAvatar(forMatrixItem: user.userId, withDisplayName: user.displayname) + 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) @@ -115,11 +115,11 @@ class RoomTypingBubbleCell: UITableViewCell { self.setNeedsLayout() } - private func firstUserNameFor(_ typingUsers: Array) -> String? { + private func firstUserNameFor(_ typingUsers: Array) -> String? { guard let firstUser = typingUsers.first else { return nil } - return firstUser.displayname.isEmptyOrNil ? firstUser.userId : firstUser.displayname + return firstUser.displayName.isEmptyOrNil ? firstUser.userId : firstUser.displayName } } diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index f8ff71ea5..7daa40759 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -31,3 +31,4 @@ #import "AuthFallBackViewController.h" #import "CallViewController.h" #import "MatrixContactsDataSource.h" +#import "TypingUserInfo.h" From 370db5372dba621a19332727b4d226c8699ef9ec Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 7 Apr 2021 13:11:07 +0200 Subject: [PATCH 17/38] too much vertical whitespace when replying --- .../Room/Views/InputToolbar/RoomInputToolbarView.m | 2 +- .../Room/Views/InputToolbar/RoomInputToolbarView.xib | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index aef222b14..ec9afdc99 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -27,7 +27,7 @@ #import "WidgetManager.h" #import "IntegrationManagerViewController.h" -const double RoomInputToolbarViewContextBarHeight = 30; +const double RoomInputToolbarViewContextBarHeight = 20; @interface RoomInputToolbarView() { diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index e99b0ef66..403a83a59 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -45,16 +45,16 @@ - +