From 93fd5cd8353b47cd5c1f66ffcdba4e29f3bc2553 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 2 Jun 2021 07:36:32 +0200 Subject: [PATCH 001/125] Voice Messages - Hold and send - Added voice message button - voice recording UI state --- .../Contents.json | 26 +++++ .../action_voice_message.png | Bin 0 -> 694 bytes .../action_voice_message@2x.png | Bin 0 -> 1215 bytes .../action_voice_message@3x.png | Bin 0 -> 1700 bytes Riot/Generated/Images.swift | 1 + .../Views/InputToolbar/RoomInputToolbarView.h | 6 +- .../Views/InputToolbar/RoomInputToolbarView.m | 39 ++++--- .../InputToolbar/RoomInputToolbarView.xib | 31 +++-- .../Views/InputToolbar/VoiceRecordView.swift | 107 ++++++++++++++++++ .../Views/InputToolbar/VoiceRecordView.xib | 44 +++++++ 10 files changed, 230 insertions(+), 24 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/action_voice_message.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/action_voice_message@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/action_voice_message@3x.png create mode 100644 Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift create mode 100644 Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/Contents.json new file mode 100644 index 0000000000..ead86edbb9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "action_voice_message.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_voice_message@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_voice_message@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_voice_message.imageset/action_voice_message.png b/Riot/Assets/Images.xcassets/Room/Actions/action_voice_message.imageset/action_voice_message.png new file mode 100644 index 0000000000000000000000000000000000000000..b969cb3aadc0d24104955f0be41fec5407b6fc4d GIT binary patch literal 694 zcmV;n0!jUeP)`7)^LDHm1u;faK8?bQ>kQ2ZWbOhxFxO1?0 zn|%+Y2-`@q$L{>iFu?wNZ{MfYE z6hs8JQ=Iq_@J#&l#I|l1W!YU;aB01WdYwjN3J-ug7*seax71v{wm9-^MYfBK+bG?x zW~ESKJa_=qm9>q;JrW{@l0$%)^eiF2UHpo|qJ*|Z=7v&NHMYqdc||x^f09!H9sS*D zHbQY1k!0H0dmk7wi{8XWIpmUYP2c^CArY+TWfF{3u^i~c`)?&k z$sTOh8u8y-JFoALdPTNJ`dktAmKDwcOVd5Ca{`nAyjP{5;?CjMWc{zo~Ce!~Hg&8OCn9#kh cKSksJ0XY@-(8QVX*#H0l07*qoM6N<$f=9JDW)V38e68G3 ztE{#almz+CO;NG}J|1i2yEjl0r3fGWdv(C7YCu^KHM%F4Jt%>~N1}H;sFQNug8_t^ zR$uIH-WHK5tl4j6)M!4?!mVZut`Q!oS~TodChB2$QP@A#rQ-+dKA4Scs=M4%91 z4XO@?p~J9{@s@ULkA)@SMPo^L(O42*G?s)HjV0klHuw=e8az2ov0V%vWI=Of+nFTx&qO4|QzZEV7`dTuaC@mAh z5iz2ETmT2fh5Mrd>oqJp3!}T%#0?R zu;s>tNrjb5P1Dd^&WexN^{n|!Sb3?7O{FiE8{UFp*ek*C;s2qc3=NZ~lrR@$x zGz_i1bP@V5dba%0kghQ#+@995T$4edR>w8p!es9-Q0tScb^`JFN~x`M9zp*zVJXOE zZL?*mnM&gyzdaj{wUnQ<7#Yj_*k;+8@05L$B^Qe&1UjTcNS8<~kIEv?cQ&7p)-72y z>w!|K-2ZiBby~B6UbHsW)8ro-XRO@XJ-y5>QY$X`|MD#L#pGHIIKo2qrKqi1sA1^* zF!Hjh?NLy}aRQ`*BBdk}{_F3nhHcYZ$N)E4`ujT@ZK#MSW*9jKyxv~cJkarX;1{CW z!h<|(ORKECv$;Myp0u#^-8g{GGK4tD6EA~W4UzP_&%;r+K(!EcL!tQdytQTCL6^Nr z!r4#lag2Kz;%f4eHU#8GWazJ_9k8Cs%bF20$dG4( ztl+Dp`H0pBFa%?S)ygQ`P z1|F)<)yb4kwgswzB@n|<$7xTt@CpK&x|z|ihQqn^6%@EIrXGTYcL?-{)1~l^oO!p{9

DCg7A%1Q_qnr{##NWdSmId(LEWbVj@!Lx$mCg`m=! zb+(f;eMbfd*Z2}*CWnzj~)`Mfh7uz!y3bb0%O)^W2Zx%lsy;`QG5d2Vb5`JrYFnu-t$ z3JI#qHry+!Z|)lFm;fEMh;}8?gRB8RRFXZYyF^wz$L%Rq(?@p(d?j3#GlDZm<9fp@ z%viQ=cdw(H6~RZZrJK^lh!B>&ZhYe@>aDT1NXcSv znRDn8LSw>aPhIG97KPM4dxWNE+C!)fPI77rA)atAVqWg`jinBIRMOMUJL=U-SU<6W zss?Egr@>IgS%uf1F?;)+r=W~^!|9V@O8&Ep@7vx;^|P6KLWZ-4wIzd1tvK~;D)f6T zXnihw0aA~og4g({6Nzp;o^H4_WMQ{uqJ^Q_=R)O~zQF&E@xEf{b)U^|L$N~)G1+5F zX^G~ttSd}6g7=iO`Dh5V1Nw->H|hW7v!thMdLs9Gj5Y_;F_^4p9vNf)BWIc{TglQy>0bz!kPsg|J=gnE&2H*;TsdXnTpM(~v6kPFu#}mC6fm zhYK7n^u7%7W=!^aq&4ID+mw{F$%u5&@P&>R=#BaENWZMArqQa0N^9-=Pwr7PA;$!(ArCwYU`KQXN74e+DzWrY#v-8v2yYw*Jv_CS>cO~4@ zEr}q;i=hqI$jx)PgR8Q)ZD~qeTPzpyctyk_I-OxtDO^VfwlGEKPJC=Coz!3HKPPU(wv<}rA@_t_SW7K?^U2@?5e9p7ESR*wb-=9#1z zO?AW!)jzoujwxkt(PQlH+=U3g)-F^*kXs1=zl)7pmJf>Kd-V5_ue^MOmT(iyptis;u{k9=>sTT|*@ zV^k0x { // The intermediate action sheet UIAlertController *actionSheet; + + VoiceRecordView *voiceRecordView; } @end @@ -75,6 +77,13 @@ - (void)awakeFromNib [self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted]; self.isEncryptionEnabled = _isEncryptionEnabled; + + voiceRecordView = [VoiceRecordView instanceFromNib]; + voiceRecordView.delegate = self; + + voiceRecordView.frame = self.voiceRecorderContainerView.bounds; + voiceRecordView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.voiceRecorderContainerView addSubview:voiceRecordView]; } #pragma mark - Override MXKView @@ -127,6 +136,9 @@ -(void)customizeViewRendering self.inputContextLabel.textColor = ThemeService.shared.theme.textSecondaryColor; self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor; [self.actionsBar updateWithTheme:ThemeService.shared.theme]; + + self.voiceRecorderContainerView.backgroundColor = ThemeService.shared.theme.backgroundColor; + [voiceRecordView updateWithTheme:ThemeService.shared.theme]; } #pragma mark - @@ -358,18 +370,10 @@ - (void)updateSendButtonWithMessage:(NSString *)textMessage { self.actionMenuOpened = NO; - if (textMessage.length) - { - self.rightInputToolbarButton.alpha = 1; - self.messageComposerContainerTrailingConstraint.constant = self.frame.size.width - self.rightInputToolbarButton.frame.origin.x + 12; - } - else - { - self.rightInputToolbarButton.alpha = 0; - self.messageComposerContainerTrailingConstraint.constant = 12; - } - - [self layoutIfNeeded]; + [UIView animateWithDuration:.15 animations:^{ + self.rightInputToolbarButton.alpha = textMessage.length ? 1 : 0; + self.voiceRecorderContainerView.alpha = textMessage.length ? 0 : 1; + }]; } #pragma mark - properties @@ -432,4 +436,13 @@ - (void)paste:(id)sender [super paste:sender]; } +#pragma mark - VoiceRecordViewDelegate + +- (void)voiceRecordViewExpandedStateDidChange:(VoiceRecordView * _Nonnull)voiceRecordView { + [UIView animateWithDuration:voiceRecordView.expandAnimationDuration animations:^{ + self.voiceRecorderContainerWidthConstraint.constant = voiceRecordView.isExpanded ? self.bounds.size.width : 48; + [self layoutIfNeeded]; + }]; +} + @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index aef8b2f816..a1daff4878 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -4,6 +4,7 @@ + @@ -36,25 +37,25 @@ - + - + - + + + + + + + + + - + + + @@ -150,8 +161,9 @@ - + + @@ -162,5 +174,8 @@ + + + diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift new file mode 100644 index 0000000000..6537c1b978 --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift @@ -0,0 +1,107 @@ +// +// 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 + +@objc protocol VoiceRecordViewDelegate: NSObjectProtocol { + func voiceRecordViewExpandedStateDidChange(_ voiceRecordView: VoiceRecordView) +} + +@objcMembers +class VoiceRecordView: UIView, Themable { + + @IBOutlet var voiceMessageButton: UIImageView! + @IBOutlet var voiceMessageButtonTrailingConstraint: NSLayoutConstraint! + + weak var delegate: VoiceRecordViewDelegate? + var isExpanded = false { + didSet { + delegate?.voiceRecordViewExpandedStateDidChange(self) + } + } + let expandAnimationDuration = 0.3 + + private var firstTouchPoint: CGPoint = CGPoint.zero + private var initialVoiceMessageButtonPadding: CGFloat = 0 + + // MARK: - Themable + + func update(theme: Theme) { + voiceMessageButton.tintColor = theme.tintColor + } + + // MARK: - Instanciation + + class func instanceFromNib() -> VoiceRecordView { + let nib = UINib(nibName: "VoiceRecordView", bundle: nil) + guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { + fatalError("The nib \(nib) expected its root view to be of type \(self)") + } + return view + } + + override func awakeFromNib() { + super.awakeFromNib() + + initialVoiceMessageButtonPadding = voiceMessageButtonTrailingConstraint.constant + } + + // MARK: - Touch management + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + let point = touches.first?.location(in: self) ?? CGPoint.zero + firstTouchPoint = CGPoint(x: self.bounds.width - point.x, y: point.y) + isExpanded = true + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + guard let point = touches.first?.location(in: self) else { + return + } + + let xDelta = min(firstTouchPoint.x - (self.bounds.width - point.x), 0) + UIView.animate(withDuration: 0.001) { + self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding - xDelta + self.layoutIfNeeded() + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + isExpanded = false + + UIView.animate(withDuration: expandAnimationDuration) { + self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding + self.layoutIfNeeded() + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + isExpanded = false + + UIView.animate(withDuration: expandAnimationDuration) { + self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding + self.layoutIfNeeded() + } + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib new file mode 100644 index 0000000000..a2f71b9d97 --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3624d2ec44a09c2353b98ba564b26d0012f5f885 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 3 Jun 2021 15:04:38 +0300 Subject: [PATCH 002/125] #4090 - New voice recording toolbar ui and swipe to cancel interaction. --- .../Room/VoiceMessages/Contents.json | 6 + .../Contents.json | 23 +++ .../voice_message_cancel_fade.png | Bin 0 -> 1360 bytes .../voice_message_cancel_fade@2x.png | Bin 0 -> 4570 bytes .../voice_message_cancel_fade@3x.png | Bin 0 -> 9982 bytes .../Contents.json | 0 .../action_voice_message.png | Bin .../action_voice_message@2x.png | Bin .../action_voice_message@3x.png | Bin .../Contents.json | 23 +++ .../voice_message_record_button_recording.png | Bin 0 -> 2192 bytes ...ice_message_record_button_recording@2x.png | Bin 0 -> 4029 bytes ...ice_message_record_button_recording@3x.png | Bin 0 -> 6082 bytes Riot/Generated/Images.swift | 4 +- .../Views/InputToolbar/RoomInputToolbarView.h | 3 - .../Views/InputToolbar/RoomInputToolbarView.m | 47 +++--- .../InputToolbar/RoomInputToolbarView.xib | 27 ++-- .../VoiceMessageToolbarView.swift | 143 ++++++++++++++++++ .../InputToolbar/VoiceMessageToolbarView.xib | 113 ++++++++++++++ .../Views/InputToolbar/VoiceRecordView.swift | 107 ------------- .../Views/InputToolbar/VoiceRecordView.xib | 44 ------ Riot/Utils/PassthroughView.swift | 29 ++++ 22 files changed, 370 insertions(+), 199 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade@3x.png rename Riot/Assets/Images.xcassets/Room/{Actions/action_voice_message.imageset => VoiceMessages/voice_message_record_button_default.imageset}/Contents.json (100%) rename Riot/Assets/Images.xcassets/Room/{Actions/action_voice_message.imageset => VoiceMessages/voice_message_record_button_default.imageset}/action_voice_message.png (100%) rename Riot/Assets/Images.xcassets/Room/{Actions/action_voice_message.imageset => VoiceMessages/voice_message_record_button_default.imageset}/action_voice_message@2x.png (100%) rename Riot/Assets/Images.xcassets/Room/{Actions/action_voice_message.imageset => VoiceMessages/voice_message_record_button_default.imageset}/action_voice_message@3x.png (100%) create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png create mode 100644 Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift create mode 100644 Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib delete mode 100644 Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift delete mode 100644 Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib create mode 100644 Riot/Utils/PassthroughView.swift diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json new file mode 100644 index 0000000000..a9f8f0bbae --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_cancel_fade.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_cancel_fade@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_cancel_fade@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade.png new file mode 100644 index 0000000000000000000000000000000000000000..aad31deb78bd9ebcdd258eb93b7c4ec9e978589e GIT binary patch literal 1360 zcmV-W1+V&vP);N80UqS92?`^p+oDs`r8O?18cf` z-OJ85+3e&uBFsBtytq|j2Xst{cCA)uC%o+126)+VPlgh1at^i#D_>vS^ZY1^n&HE| zo<&~^WMgZ~xXC`{dDtPW7b!hh=S7B&pv?XrZ3Fa6xXH2UZKv_GwZdD+=y%8&Me!8Q zN^v{Tir?)Vi?-bk%J!LZf{KA=HrV;{-Fc2uR78Uce^YBSDNV$eVM-Sj16|qaMbQ#D z59_?fT4X?7b9Bl=G@0gQ3OiHWkCW!b&KH{aH zXBrJuqcYBv!qEmg;vD34RU$kMqG!7pNxUBY34|@a$24f68%SwLqN zR!2OnrZ6N|#PhP7grlQofOT&+okhjli>fAmdh~dCdsLhUV)ZsV%EfFhqlAH$q(2f4~;x4(L6ZZHn~dV0)qTXw&Z}-5FinnRSOj z8`vt>Go=0ABo`rG>O^%BrOQ9x&p4=NJYdy`J=h;L2ezg4yxj6Z9&c&c_v~3LicpFp zu6m{f0qeaN_1bIw5kTXi+6`9dryO_Z??KU6l~<-FWaiNgf=xxpsBFClZ8&uPD&wf% zgHYj~<&|Ami4yD4Ubht)4L#39FT|0z*%hTBWUf>67>Yh*43S*a_kiB+B}AU3ntaC3 z4bhp1A=|>EbYfFjmap}E)bT}WxUy%6Gk$J}&P21YXOl4&74U2tL+e=7B3Ul0K}$K+ zLABZN^1SEmNO3G3p=gcvw_WBE-su^_{%hv!xXLkUF*s5TFIwP~vo&ZYomA&uK&`S(DeUyl_zl|1UhF&DBhyW$Fl(TSS418+i^%X+u`H_C zvsJXFX~Dj4*~RUwNtrrl73W2k?er`S>;71DS_kL>{oldGE-rH&;$de^s?<5;v8;_o z9TtidOWQ=;<^mb`oBme2IFx@Gc125;PgFZt>WS!kf|iZ32(9o6aYhZ=!N=>qJS9hU zhw;wHV$&wucd1!8;wE8dE%NxII-*-`Z|w@A%!0bOX@|ZAn^oX>;t`5rwX@PSqDEtk zqe#);kt}SV>G$^xRJD3VF*9HeU~2(0oRIZ;T(4yvo`(kHSc3guJr$tbzamsNEwPlr zT4%d S8IX?v00003c!1KR#P)-8SRv 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..7fdf91c21d5493754dffcde323f7da899067c2c9 GIT binary patch literal 6082 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 IM6N<$f((78Gynhq literal 0 HcmV?d00001 diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index c1fc9b6b62..7588bee9c9 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -103,7 +103,6 @@ internal enum Asset { 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 actionVoiceMessage = ImageAsset(name: "action_voice_message") internal static let error = ImageAsset(name: "error") internal static let errorMessageTick = ImageAsset(name: "error_message_tick") internal static let newClose = ImageAsset(name: "new_close") @@ -131,6 +130,9 @@ internal enum Asset { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") + internal static let voiceMessageCancelFade = ImageAsset(name: "voice_message_cancel_fade") + internal static let voiceMessageRecordButtonDefault = ImageAsset(name: "voice_message_record_button_default") + internal static let voiceMessageRecordButtonRecording = ImageAsset(name: "voice_message_record_button_recording") internal static let addMemberFloatingAction = ImageAsset(name: "add_member_floating_action") internal static let addParticipant = ImageAsset(name: "add_participant") internal static let addParticipants = ImageAsset(name: "add_participants") diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index b3d7ad7263..f1eb8c67ca 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -68,9 +68,6 @@ typedef enum : NSUInteger @property (weak, nonatomic) IBOutlet UIButton *inputContextButton; @property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar; -@property (weak, nonatomic) IBOutlet UIView *voiceRecorderContainerView; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *voiceRecorderContainerWidthConstraint; - /** Tell whether the filled data will be sent encrypted. NO by default. */ diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 36b65a1da6..d6a8d9e1ed 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -35,14 +35,15 @@ const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; -@interface RoomInputToolbarView() +@interface RoomInputToolbarView() { // The intermediate action sheet UIAlertController *actionSheet; - - VoiceRecordView *voiceRecordView; } +@property (nonatomic, weak) IBOutlet UIView *voiceMessageToolbarContainerView; +@property (nonatomic, strong) VoiceMessageToolbarView *voiceMessageToolbarView; + @end @implementation RoomInputToolbarView @@ -78,12 +79,12 @@ - (void)awakeFromNib self.isEncryptionEnabled = _isEncryptionEnabled; - voiceRecordView = [VoiceRecordView instanceFromNib]; - voiceRecordView.delegate = self; + self.voiceMessageToolbarView = [VoiceMessageToolbarView instanceFromNib]; + self.voiceMessageToolbarView.frame = self.voiceMessageToolbarContainerView.bounds; + self.voiceMessageToolbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.voiceMessageToolbarContainerView addSubview:self.voiceMessageToolbarView]; - voiceRecordView.frame = self.voiceRecorderContainerView.bounds; - voiceRecordView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.voiceRecorderContainerView addSubview:voiceRecordView]; + [self _updateUIWithTextMessage:nil animated:NO]; } #pragma mark - Override MXKView @@ -137,15 +138,14 @@ -(void)customizeViewRendering self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor; [self.actionsBar updateWithTheme:ThemeService.shared.theme]; - self.voiceRecorderContainerView.backgroundColor = ThemeService.shared.theme.backgroundColor; - [voiceRecordView updateWithTheme:ThemeService.shared.theme]; + [self.voiceMessageToolbarView updateWithTheme:ThemeService.shared.theme]; } #pragma mark - - (void)setTextMessage:(NSString *)textMessage { - [self updateSendButtonWithMessage:textMessage]; + [self _updateUIWithTextMessage:textMessage animated:YES]; [super setTextMessage:textMessage]; } @@ -302,7 +302,7 @@ - (IBAction)cancelAction:(id)sender - (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { NSString *newText = [growingTextView.text stringByReplacingCharactersInRange:range withString:text]; - [self updateSendButtonWithMessage:newText]; + [self _updateUIWithTextMessage:newText animated:YES]; return YES; } @@ -366,16 +366,6 @@ - (void)destroy [super destroy]; } -- (void)updateSendButtonWithMessage:(NSString *)textMessage -{ - self.actionMenuOpened = NO; - - [UIView animateWithDuration:.15 animations:^{ - self.rightInputToolbarButton.alpha = textMessage.length ? 1 : 0; - self.voiceRecorderContainerView.alpha = textMessage.length ? 0 : 1; - }]; -} - #pragma mark - properties - (void)setActionMenuOpened:(BOOL)actionMenuOpened @@ -436,12 +426,15 @@ - (void)paste:(id)sender [super paste:sender]; } -#pragma mark - VoiceRecordViewDelegate +#pragma mark - Private -- (void)voiceRecordViewExpandedStateDidChange:(VoiceRecordView * _Nonnull)voiceRecordView { - [UIView animateWithDuration:voiceRecordView.expandAnimationDuration animations:^{ - self.voiceRecorderContainerWidthConstraint.constant = voiceRecordView.isExpanded ? self.bounds.size.width : 48; - [self layoutIfNeeded]; +- (void)_updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated +{ + self.actionMenuOpened = NO; + + [UIView animateWithDuration:(animated ? 0.15f : 0.0f) animations:^{ + self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; + self.voiceMessageToolbarContainerView.alpha = textMessage.length ? 0.0f : 1.0; }]; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index a1daff4878..a170d73e0c 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -1,10 +1,9 @@ - + - - + @@ -17,7 +16,7 @@ - - - - - - - + + + @@ -128,6 +124,7 @@ + @@ -162,8 +159,7 @@ - - + @@ -174,8 +170,5 @@ - - - diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift new file mode 100644 index 0000000000..5b56b653ea --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift @@ -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 + +private enum VoiceMessageToolbarViewState { + case idle + case recording +} + +class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { + + @IBOutlet private var backgroundView: UIView! + + @IBOutlet private var recordButtonsContainerView: UIView! + @IBOutlet private var primaryRecordButton: UIButton! + @IBOutlet private var secondaryRecordButton: UIButton! + + @IBOutlet private var slideToCancelContainerView: UIView! + @IBOutlet private var slideToCancelLabel: UILabel! + @IBOutlet private var slideToCancelChevron: UIImageView! + @IBOutlet private var slideToCancelFade: UIImageView! + + private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 + + private var state: VoiceMessageToolbarViewState = .idle { + didSet { + updateUIAnimated(true) + } + } + + private var currentTheme: Theme? { + didSet { + updateUIAnimated(true) + } + } + + @objc static func instanceFromNib() -> VoiceMessageToolbarView { + let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil) + guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { + fatalError("The nib \(nib) expected its root view to be of type \(self)") + } + return view + } + + override func awakeFromNib() { + super.awakeFromNib() + + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + longPressGesture.delegate = self + longPressGesture.minimumPressDuration = 0.1 + recordButtonsContainerView.addGestureRecognizer(longPressGesture) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) + longPressGesture.delegate = self + recordButtonsContainerView.addGestureRecognizer(panGesture) + + updateUIAnimated(false) + } + + // MARK: - Themable + + func update(theme: Theme) { + currentTheme = theme + } + + // MARK: - UIGestureRecognizerDelegate + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + // MARK: - Private + + @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + switch gestureRecognizer.state { + case UIGestureRecognizer.State.began: + state = .recording + + let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) + cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX + + case UIGestureRecognizer.State.ended: + state = .idle + default: + break + } + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard self.state == .recording && gestureRecognizer.state == .changed else { + return + } + + let translation = gestureRecognizer.translation(in: self) + + recordButtonsContainerView.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0) + slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0) + } + + private func updateUIAnimated(_ animated: Bool) { + UIView.animate(withDuration: (animated ? 0.25 : 0.0)) { + switch self.state { + case .idle: + self.slideToCancelContainerView.alpha = 0.0 + self.backgroundView.alpha = 0.0 + self.slideToCancelFade.alpha = 0.0 + self.recordButtonsContainerView.transform = .identity + self.slideToCancelContainerView.transform = .identity + self.primaryRecordButton.alpha = 1.0 + self.secondaryRecordButton.alpha = 0.0 + case .recording: + self.slideToCancelContainerView.alpha = 1.0 + self.backgroundView.alpha = 1.0 + self.slideToCancelFade.alpha = 1.0 + self.primaryRecordButton.alpha = 0.0 + self.secondaryRecordButton.alpha = 1.0 + } + + guard let theme = self.currentTheme else { + return + } + + self.backgroundView.backgroundColor = theme.backgroundColor + self.primaryRecordButton.tintColor = theme.textSecondaryColor + self.slideToCancelLabel.textColor = theme.textSecondaryColor + self.slideToCancelChevron.tintColor = theme.textSecondaryColor + } + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib new file mode 100644 index 0000000000..2b7d80a621 --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift deleted file mode 100644 index 6537c1b978..0000000000 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// 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 - -@objc protocol VoiceRecordViewDelegate: NSObjectProtocol { - func voiceRecordViewExpandedStateDidChange(_ voiceRecordView: VoiceRecordView) -} - -@objcMembers -class VoiceRecordView: UIView, Themable { - - @IBOutlet var voiceMessageButton: UIImageView! - @IBOutlet var voiceMessageButtonTrailingConstraint: NSLayoutConstraint! - - weak var delegate: VoiceRecordViewDelegate? - var isExpanded = false { - didSet { - delegate?.voiceRecordViewExpandedStateDidChange(self) - } - } - let expandAnimationDuration = 0.3 - - private var firstTouchPoint: CGPoint = CGPoint.zero - private var initialVoiceMessageButtonPadding: CGFloat = 0 - - // MARK: - Themable - - func update(theme: Theme) { - voiceMessageButton.tintColor = theme.tintColor - } - - // MARK: - Instanciation - - class func instanceFromNib() -> VoiceRecordView { - let nib = UINib(nibName: "VoiceRecordView", bundle: nil) - guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { - fatalError("The nib \(nib) expected its root view to be of type \(self)") - } - return view - } - - override func awakeFromNib() { - super.awakeFromNib() - - initialVoiceMessageButtonPadding = voiceMessageButtonTrailingConstraint.constant - } - - // MARK: - Touch management - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - - let point = touches.first?.location(in: self) ?? CGPoint.zero - firstTouchPoint = CGPoint(x: self.bounds.width - point.x, y: point.y) - isExpanded = true - } - - override func touchesMoved(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - - guard let point = touches.first?.location(in: self) else { - return - } - - let xDelta = min(firstTouchPoint.x - (self.bounds.width - point.x), 0) - UIView.animate(withDuration: 0.001) { - self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding - xDelta - self.layoutIfNeeded() - } - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - super.touchesEnded(touches, with: event) - - isExpanded = false - - UIView.animate(withDuration: expandAnimationDuration) { - self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding - self.layoutIfNeeded() - } - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - super.touchesCancelled(touches, with: event) - - isExpanded = false - - UIView.animate(withDuration: expandAnimationDuration) { - self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding - self.layoutIfNeeded() - } - } -} diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib deleted file mode 100644 index a2f71b9d97..0000000000 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Utils/PassthroughView.swift b/Riot/Utils/PassthroughView.swift new file mode 100644 index 0000000000..b101c7ea5f --- /dev/null +++ b/Riot/Utils/PassthroughView.swift @@ -0,0 +1,29 @@ +// +// 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 + +class PassthroughView: UIView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let hitTarget = super.hitTest(point, with: event) + + guard hitTarget == self else { + return hitTarget + } + + return nil + } +} From 3015cba8dcb6b3cb064150c5363b43cc718ecab1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 3 Jun 2021 16:58:37 +0300 Subject: [PATCH 003/125] #4090 - Corrected dark theme appearance. --- .../Contents.json | 6 +++--- .../voice_message_cancel_gradient.png} | Bin .../voice_message_cancel_gradient@2x.png} | Bin .../voice_message_cancel_gradient@3x.png} | Bin Riot/Generated/Images.swift | 2 +- .../Room/Views/InputToolbar/RoomInputToolbarView.m | 13 ++++++++----- .../Views/InputToolbar/RoomInputToolbarView.xib | 9 --------- .../InputToolbar/VoiceMessageToolbarView.swift | 9 ++++++--- .../Views/InputToolbar/VoiceMessageToolbarView.xib | 6 +++--- 9 files changed, 21 insertions(+), 24 deletions(-) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_cancel_fade.imageset => voice_message_cancel_gradient.imageset}/Contents.json (60%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_cancel_fade.imageset/voice_message_cancel_fade.png => voice_message_cancel_gradient.imageset/voice_message_cancel_gradient.png} (100%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_cancel_fade.imageset/voice_message_cancel_fade@2x.png => voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@2x.png} (100%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_cancel_fade.imageset/voice_message_cancel_fade@3x.png => voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@3x.png} (100%) diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/Contents.json similarity index 60% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/Contents.json index a9f8f0bbae..1d88a1deb5 100644 --- a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "voice_message_cancel_fade.png", + "filename" : "voice_message_cancel_gradient.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "voice_message_cancel_fade@2x.png", + "filename" : "voice_message_cancel_gradient@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "voice_message_cancel_fade@3x.png", + "filename" : "voice_message_cancel_gradient@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient.png similarity index 100% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade.png rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient.png diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@2x.png similarity index 100% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade@2x.png rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@2x.png diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@3x.png similarity index 100% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_fade.imageset/voice_message_cancel_fade@3x.png rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@3x.png diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 7588bee9c9..4888dcb5da 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -130,7 +130,7 @@ internal enum Asset { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") - internal static let voiceMessageCancelFade = ImageAsset(name: "voice_message_cancel_fade") + internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") internal static let voiceMessageRecordButtonDefault = ImageAsset(name: "voice_message_record_button_default") internal static let voiceMessageRecordButtonRecording = ImageAsset(name: "voice_message_record_button_recording") internal static let addMemberFloatingAction = ImageAsset(name: "add_member_floating_action") diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index d6a8d9e1ed..fe573c6b13 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -41,7 +41,6 @@ @interface RoomInputToolbarView() UIAlertController *actionSheet; } -@property (nonatomic, weak) IBOutlet UIView *voiceMessageToolbarContainerView; @property (nonatomic, strong) VoiceMessageToolbarView *voiceMessageToolbarView; @end @@ -80,9 +79,13 @@ - (void)awakeFromNib self.isEncryptionEnabled = _isEncryptionEnabled; self.voiceMessageToolbarView = [VoiceMessageToolbarView instanceFromNib]; - self.voiceMessageToolbarView.frame = self.voiceMessageToolbarContainerView.bounds; - self.voiceMessageToolbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.voiceMessageToolbarContainerView addSubview:self.voiceMessageToolbarView]; + self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:self.voiceMessageToolbarView]; + + [NSLayoutConstraint activateConstraints:@[[self.mainToolbarView.topAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.topAnchor], + [self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor], + [self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor], + [self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]]; [self _updateUIWithTextMessage:nil animated:NO]; } @@ -434,7 +437,7 @@ - (void)_updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated [UIView animateWithDuration:(animated ? 0.15f : 0.0f) animations:^{ self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; - self.voiceMessageToolbarContainerView.alpha = textMessage.length ? 0.0f : 1.0; + self.voiceMessageToolbarView.alpha = textMessage.length ? 0.0f : 1.0; }]; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index a170d73e0c..1056cd289b 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -106,17 +106,12 @@ - - - - - @@ -124,11 +119,8 @@ - - - @@ -159,7 +151,6 @@ - diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift index 5b56b653ea..959b63a74c 100644 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift @@ -32,7 +32,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel @IBOutlet private var slideToCancelContainerView: UIView! @IBOutlet private var slideToCancelLabel: UILabel! @IBOutlet private var slideToCancelChevron: UIImageView! - @IBOutlet private var slideToCancelFade: UIImageView! + @IBOutlet private var slideToCancelGradient: UIImageView! private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 @@ -59,6 +59,8 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel override func awakeFromNib() { super.awakeFromNib() + slideToCancelGradient.image = Asset.Images.voiceMessageCancelGradient.image.withRenderingMode(.alwaysTemplate) + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) longPressGesture.delegate = self longPressGesture.minimumPressDuration = 0.1 @@ -117,7 +119,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel case .idle: self.slideToCancelContainerView.alpha = 0.0 self.backgroundView.alpha = 0.0 - self.slideToCancelFade.alpha = 0.0 + self.slideToCancelGradient.alpha = 0.0 self.recordButtonsContainerView.transform = .identity self.slideToCancelContainerView.transform = .identity self.primaryRecordButton.alpha = 1.0 @@ -125,7 +127,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel case .recording: self.slideToCancelContainerView.alpha = 1.0 self.backgroundView.alpha = 1.0 - self.slideToCancelFade.alpha = 1.0 + self.slideToCancelGradient.alpha = 1.0 self.primaryRecordButton.alpha = 0.0 self.secondaryRecordButton.alpha = 1.0 } @@ -138,6 +140,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel self.primaryRecordButton.tintColor = theme.textSecondaryColor self.slideToCancelLabel.textColor = theme.textSecondaryColor self.slideToCancelChevron.tintColor = theme.textSecondaryColor + self.slideToCancelGradient.tintColor = theme.backgroundColor } } } diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib index 2b7d80a621..e74a794425 100644 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib @@ -36,7 +36,7 @@ - + @@ -98,7 +98,7 @@ - + @@ -106,7 +106,7 @@ - + From b5070975d0645125250f7ee2157b4659134365f3 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 4 Jun 2021 15:17:34 +0300 Subject: [PATCH 004/125] #4090 - Corrected dark theme appearance. --- .../Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib index e74a794425..cb7a3ae30c 100644 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib @@ -19,7 +19,7 @@ - + From 8d54e035271c1288d362f39f7c75d64b3fe88b20 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 7 Jun 2021 10:20:26 +0300 Subject: [PATCH 005/125] #4090 - Add voice message controller, audio recorder and toolbar view links. Working audio file sending and cancellation. --- Riot/Modules/Room/RoomViewController.m | 23 +++- .../Views/InputToolbar/RoomInputToolbarView.h | 1 + .../Views/InputToolbar/RoomInputToolbarView.m | 15 ++- .../Room/VoiceMessages/AudioPlayer.swift | 17 +++ .../Room/VoiceMessages/AudioRecorder.swift | 82 +++++++++++++ .../VoiceMessageController.swift | 109 ++++++++++++++++++ .../VoiceMessageToolbarView.swift | 43 +++++-- .../VoiceMessageToolbarView.xib | 0 Riot/Utils/PassthroughView.swift | 2 +- 9 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 Riot/Modules/Room/VoiceMessages/AudioPlayer.swift create mode 100644 Riot/Modules/Room/VoiceMessages/AudioRecorder.swift create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift rename Riot/Modules/Room/{Views/InputToolbar => VoiceMessages}/VoiceMessageToolbarView.swift (78%) rename Riot/Modules/Room/{Views/InputToolbar => VoiceMessages}/VoiceMessageToolbarView.xib (100%) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index fb4b7db8f9..9869b17a34 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -135,7 +135,7 @@ @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate> { // The preview header @@ -240,6 +240,8 @@ @interface RoomViewController () Void) +} + +public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, AudioRecorderDelegate { + + private let themeService: ThemeService + private let _voiceMessageToolbarView: VoiceMessageToolbarView + + private var audioRecorder: AudioRecorder? + + @objc public weak var delegate: VoiceMessageControllerDelegate? + + @objc public var voiceMessageToolbarView: UIView { + return _voiceMessageToolbarView + } + + @objc public init(themeService: ThemeService) { + _voiceMessageToolbarView = VoiceMessageToolbarView.instanceFromNib() + self.themeService = themeService + + super.init() + + _voiceMessageToolbarView.delegate = self + + self._voiceMessageToolbarView.update(theme: self.themeService.theme) + NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + // MARK: - VoiceMessageToolbarViewDelegate + + func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) { + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString) + + audioRecorder = AudioRecorder() + audioRecorder?.delegate = self + audioRecorder?.recordWithOuputURL(temporaryFileURL) + } + + func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { + audioRecorder?.stopRecording() + + guard let url = audioRecorder?.url else { + MXLog.error("Invalid audio recording URL") + return + } + + delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url) { [weak self] success in + self?.deleteRecordingAtURL(url) + } + } + + func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { + audioRecorder?.stopRecording() + deleteRecordingAtURL(audioRecorder?.url) + } + + // MARK: - AudioRecorderDelegate + + func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) { + _voiceMessageToolbarView.state = .recording + } + + func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) { + _voiceMessageToolbarView.state = .idle + } + + func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) { + MXLog.error("Failed recording voice message.") + _voiceMessageToolbarView.state = .idle + } + + // MARK: - Private + + private func deleteRecordingAtURL(_ url: URL?) { + guard let url = url else { + return + } + + do { + try FileManager.default.removeItem(at: url) + } catch { + MXLog.error(error) + } + } + + @objc private func handleThemeDidChange() { + self._voiceMessageToolbarView.update(theme: self.themeService.theme) + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift similarity index 78% rename from Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift rename to Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 959b63a74c..a01a0ff7d4 100644 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -16,12 +16,20 @@ import UIKit -private enum VoiceMessageToolbarViewState { +protocol VoiceMessageToolbarViewDelegate: AnyObject { + func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) +} + +enum VoiceMessageToolbarViewState { case idle case recording } class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { + + weak var delegate: VoiceMessageToolbarViewDelegate? @IBOutlet private var backgroundView: UIView! @@ -36,14 +44,22 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 - private var state: VoiceMessageToolbarViewState = .idle { + private var currentTheme: Theme? { didSet { updateUIAnimated(true) } } - private var currentTheme: Theme? { + var state: VoiceMessageToolbarViewState = .idle { didSet { + switch state { + case .recording: + let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) + cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX + case .idle: + cancelDrag() + } + updateUIAnimated(true) } } @@ -90,13 +106,11 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { switch gestureRecognizer.state { case UIGestureRecognizer.State.began: - state = .recording - - let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) - cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX - + delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) case UIGestureRecognizer.State.ended: - state = .idle + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + case UIGestureRecognizer.State.cancelled: + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) default: break } @@ -111,6 +125,17 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel recordButtonsContainerView.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0) slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0) + + if abs(translation.x) > self.bounds.width / 2.0 { + cancelDrag() + } + } + + private func cancelDrag() { + recordButtonsContainerView.gestureRecognizers?.forEach { gestureRecognizer in + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + } } private func updateUIAnimated(_ animated: Bool) { diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib similarity index 100% rename from Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib rename to Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib diff --git a/Riot/Utils/PassthroughView.swift b/Riot/Utils/PassthroughView.swift index b101c7ea5f..2d89fc8f7f 100644 --- a/Riot/Utils/PassthroughView.swift +++ b/Riot/Utils/PassthroughView.swift @@ -17,7 +17,7 @@ import UIKit class PassthroughView: UIView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let hitTarget = super.hitTest(point, with: event) guard hitTarget == self else { From 58c1be04a66b5b738acc45cf4412cf53cc05f903 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 8 Jun 2021 10:04:44 +0300 Subject: [PATCH 006/125] #4090 - Added recording duration label and permissions checking. --- .../Contents.json | 23 +++++ .../voice_message_record_icon.png | Bin 0 -> 247 bytes .../voice_message_record_icon@2x.png | Bin 0 -> 427 bytes .../voice_message_record_icon@3x.png | Bin 0 -> 576 bytes Riot/Generated/Images.swift | 1 + Riot/Modules/Room/RoomViewController.m | 14 +++ .../Room/VoiceMessages/AudioRecorder.swift | 8 +- .../VoiceMessageController.swift | 41 +++++++-- .../VoiceMessageToolbarView.swift | 29 +++--- .../VoiceMessages/VoiceMessageToolbarView.xib | 85 +++++++++++------- 10 files changed, 149 insertions(+), 52 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json new file mode 100644 index 0000000000..521b5c2dc5 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_record_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_record_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_record_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fb7a1660cc23f6e3ab9747bf22f8a2bf3330aae4 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4aTa()7Bet#3xhBt!>l*8o|0J>k`8}R4jv*C{y%P@d9x@Pde(Q4~(8}osqlJRX zjU{JQ8d|Ct+D>rDE|r#OSMpo&a(9jD|C7IrKDr*yZMo5yp=k2&)I4*(^Vv6xr+t%M zBKKdQny=qQwk-X%xQ=6P(^BEAA4?tx1}@iqw8bf>%VM3t=KPRVS?e@E{5#dXX^PkM pKYO3g@?ySNTy!de$P?h9GC9cxG-p+?*T$; zD}i>tP1Bj_H-Fx|S3(9H_!UgH0x<7`xGGKFylVs|DeyPvwjI<9@dayx@tn78}&}+3b`d6%w6PyuIT3Od?DmUEzsFe1d5O7dKk@w z&DtxL2cMTQ%9GvFe02eevyIri8c?Yf_?5VH{cX$SlQK5x9Mk;)PD;RGg zy-u+9--t9(_!stT`ys9`eGxC}W9w$gPvE-1g2UE^yqV z!SL+&HCAxUY~<1e#buV)A);ILUk7Bpa~3BroBIv6$ANl0aXBJCQ+Uea(Jm$@d*72YlF zz;LG!%_jj*&(YZnv|^F}L_ZwFW;)^zFUq*sVJT(9H);X-!nSS%@i z-~W->p5{ZH*)0!JMIO(n z0>pjzf_b2u{Ba60eMkC;I)TliLhPaj?_tqs@^_qPp2Y$}qw}83@Wl-|+x~RMIV0Ko zC!Mr~YOMnaESx^_X32CXR2jKO`bs)e?Bv6-Rt}nuCip5^+JL)sIPT2u{s3N=BrwQA zDM`xbWVx`9+pI@%oW6frNnDC#&8qxjSz6;&(Wd3fmDE_V2qzy2pLqo7T(71LdV<6N O0000 Void) func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, completion: @escaping (Bool) -> Void) } @@ -24,6 +25,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private let themeService: ThemeService private let _voiceMessageToolbarView: VoiceMessageToolbarView + private let timeFormatter: DateFormatter + private var displayLink: CADisplayLink! private var audioRecorder: AudioRecorder? @@ -36,11 +39,18 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, @objc public init(themeService: ThemeService) { _voiceMessageToolbarView = VoiceMessageToolbarView.instanceFromNib() self.themeService = themeService + self.timeFormatter = DateFormatter() super.init() _voiceMessageToolbarView.delegate = self + timeFormatter.dateFormat = "m:ss" + + displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLinkTick)) + displayLink.isPaused = true + displayLink.add(to: .current, forMode: .common) + self._voiceMessageToolbarView.update(theme: self.themeService.theme) NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil) } @@ -48,12 +58,18 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - VoiceMessageToolbarViewDelegate func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) { - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString) - - audioRecorder = AudioRecorder() - audioRecorder?.delegate = self - audioRecorder?.recordWithOuputURL(temporaryFileURL) + delegate?.voiceMessageController(self, didRequestPermissionCheckWithCompletion: { [weak self] success in + guard let self = self, success != false else { + return + } + + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString) + + self.audioRecorder = AudioRecorder() + self.audioRecorder?.delegate = self + self.audioRecorder?.recordWithOuputURL(temporaryFileURL) + }) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -65,6 +81,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url) { [weak self] success in + UINotificationFeedbackGenerator().notificationOccurred( (success ? .success : .error)) self?.deleteRecordingAtURL(url) } } @@ -72,21 +89,25 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { audioRecorder?.stopRecording() deleteRecordingAtURL(audioRecorder?.url) + UINotificationFeedbackGenerator().notificationOccurred(.error) } // MARK: - AudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) { _voiceMessageToolbarView.state = .recording + self.displayLink.isPaused = false } func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) { _voiceMessageToolbarView.state = .idle + displayLink.isPaused = true } func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) { MXLog.error("Failed recording voice message.") _voiceMessageToolbarView.state = .idle + displayLink.isPaused = true } // MARK: - Private @@ -106,4 +127,12 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, @objc private func handleThemeDidChange() { self._voiceMessageToolbarView.update(theme: self.themeService.theme) } + + @objc private func handleDisplayLinkTick() { + guard let audioRecorder = audioRecorder else { + return + } + + _voiceMessageToolbarView.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioRecorder.currentTime)) + } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index a01a0ff7d4..70816fefbe 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -28,20 +28,21 @@ enum VoiceMessageToolbarViewState { } class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { - - weak var delegate: VoiceMessageToolbarViewDelegate? - @IBOutlet private var backgroundView: UIView! @IBOutlet private var recordButtonsContainerView: UIView! @IBOutlet private var primaryRecordButton: UIButton! @IBOutlet private var secondaryRecordButton: UIButton! + @IBOutlet private var recordingChromeContainerView: UIView! + @IBOutlet private var slideToCancelContainerView: UIView! @IBOutlet private var slideToCancelLabel: UILabel! @IBOutlet private var slideToCancelChevron: UIImageView! @IBOutlet private var slideToCancelGradient: UIImageView! + @IBOutlet private var elapsedTimeLabel: UILabel! + private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 private var currentTheme: Theme? { @@ -50,6 +51,8 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } } + weak var delegate: VoiceMessageToolbarViewDelegate? + var state: VoiceMessageToolbarViewState = .idle { didSet { switch state { @@ -63,6 +66,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel updateUIAnimated(true) } } + + var elapsedTime: String? { + didSet { + elapsedTimeLabel.text = elapsedTime + } + } @objc static func instanceFromNib() -> VoiceMessageToolbarView { let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil) @@ -142,19 +151,17 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel UIView.animate(withDuration: (animated ? 0.25 : 0.0)) { switch self.state { case .idle: - self.slideToCancelContainerView.alpha = 0.0 self.backgroundView.alpha = 0.0 - self.slideToCancelGradient.alpha = 0.0 - self.recordButtonsContainerView.transform = .identity - self.slideToCancelContainerView.transform = .identity self.primaryRecordButton.alpha = 1.0 self.secondaryRecordButton.alpha = 0.0 + self.recordingChromeContainerView.alpha = 0.0 + self.recordButtonsContainerView.transform = .identity + self.slideToCancelContainerView.transform = .identity case .recording: - self.slideToCancelContainerView.alpha = 1.0 self.backgroundView.alpha = 1.0 - self.slideToCancelGradient.alpha = 1.0 self.primaryRecordButton.alpha = 0.0 self.secondaryRecordButton.alpha = 1.0 + self.recordingChromeContainerView.alpha = 1.0 } guard let theme = self.currentTheme else { @@ -162,10 +169,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } self.backgroundView.backgroundColor = theme.backgroundColor + self.slideToCancelGradient.tintColor = theme.backgroundColor + self.primaryRecordButton.tintColor = theme.textSecondaryColor self.slideToCancelLabel.textColor = theme.textSecondaryColor self.slideToCancelChevron.tintColor = theme.textSecondaryColor - self.slideToCancelGradient.tintColor = theme.backgroundColor + self.elapsedTimeLabel.textColor = theme.textSecondaryColor } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index cb7a3ae30c..55946887db 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -23,78 +23,96 @@ - + - - + + - - + + - - + + + + - - - - - - - - - - - - - - + + - + + - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -109,5 +127,6 @@ + From ff3170e03152a210be3e4978855de5daada8c386 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 14 Jun 2021 17:47:59 +0300 Subject: [PATCH 007/125] #4096 - Added voice message decoding, timeline cell and playback UI. --- Podfile | 1 + .../Contents.json | 23 ++ .../voice_message_pause_button.png | Bin 0 -> 467 bytes .../voice_message_pause_button@2x.png | Bin 0 -> 917 bytes .../voice_message_pause_button@3x.png | Bin 0 -> 1393 bytes .../Contents.json | 23 ++ .../voice_message_play_button.png | Bin 0 -> 647 bytes .../voice_message_play_button@2x.png | Bin 0 -> 1149 bytes .../voice_message_play_button@3x.png | Bin 0 -> 1688 bytes Riot/Generated/Images.swift | 2 + .../Files/Views/FilesSearchTableViewCell.m | 3 + .../Room/CellData/RoomBubbleCellData.m | 3 + Riot/Modules/Room/RoomViewController.m | 19 +- .../VoiceMessage/VoiceMessageBubbleCell.swift | 52 +++++ ...MessageWithPaginationTitleBubbleCell.swift | 25 ++ ...eMessageWithoutSenderInfoBubbleCell.swift} | 8 + .../VoiceMessageAudioPlayer.swift | 191 ++++++++++++++++ ....swift => VoiceMessageAudioRecorder.swift} | 20 +- .../VoiceMessageController.swift | 36 +-- .../VoiceMessagePlaybackView.swift | 215 ++++++++++++++++++ .../VoiceMessagePlaybackView.xib | 79 +++++++ .../VoiceMessageToolbarView.swift | 4 +- .../VoiceMessageWaveformView.swift | 112 +++++++++ 23 files changed, 783 insertions(+), 33 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png create mode 100644 Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift create mode 100644 Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.swift rename Riot/Modules/Room/{VoiceMessages/AudioPlayer.swift => Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.swift} (75%) create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift rename Riot/Modules/Room/VoiceMessages/{AudioRecorder.swift => VoiceMessageAudioRecorder.swift} (72%) create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift diff --git a/Podfile b/Podfile index 6746493b40..0b963c87f6 100644 --- a/Podfile +++ b/Podfile @@ -69,6 +69,7 @@ abstract_target 'RiotPods' do pod 'SwiftBase32', '~> 0.9.0' pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' + pod 'DSWaveformImage', '~> 6.1.1' pod 'FLEX', '~> 4.4.1', :configurations => ['Debug'] diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json new file mode 100644 index 0000000000..21dd49f04a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_pause_button.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_pause_button@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_pause_button@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png new file mode 100644 index 0000000000000000000000000000000000000000..f273f9e7377ff253a7060d9b44763d6187047597 GIT binary patch literal 467 zcmV;^0WAKBP){{|*W)hH$BIV6| zD;e3|ueEHg6dWOs zls0?bqpm3&HMwSkPb?^G@Ue*^_Q+3}(BqhD6kMLu2h8lgBn|6!cuQuZj!lH%urW5n zpFRUNEj8k@;4Cyl2A&X~dpRik>|2*Bp37IX?Gbr4$K%7DbKe%Z==;0S$h&sQ*2ltzH<`4>|@5_QOeQg;9V002ov JPDHLkV1kw9!g2rr literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..303f6abd9382368277b1b707ef9ac5227b3e2081 GIT binary patch literal 917 zcmV;G18V$GK~#7F?VCMv z+CUV?A8taL44eSDP7#<%l_sLfOo}8&z$dV8U^s!v2@E%|D^Dt@Qf0=tNGh zFYk{QB7_tP&Z#@Y-(*@~-gLGGRE=dO*e2S{VbymM>Sq_p5qEPaNss##BowS2a z(hgA3teW$ny^9iJ;o$pTb*^Y1ILPfyA@S=_OK{$i)jb83785`vs}onTO3DK!WRCLy zzQpDSTLIvpXz7r7xN%8gBMB(!-DXD zXyMg$-X4sxSO!3(Fr}Q!1D`&+)NKBJ@Ou5lvUywAc{}1OpEvpi`m)k9z^HtjNdQLp z5e@*_1<*NSL)X}PtiZZ5r25LflKG6o10|x}?7wd)tuu&aWDj1Nt45GtilEw(3 zGtMQmK+b;}-I>%Nazq2`F*{42prdDA!>UvDoVVuSJoe?GUx%>=CCvt-ewsp!(CTmMH8poSqAKXDTIQF$tDDiFFAQ9j4x;9&Cfi1V8~4TZU+flLLVL*O8D rYAeI!D{QAtpGS8cI&7x8r_cWZ#EQt=GZa8S00000NkvXXu0mjfe95J- literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..24539f3d5c3110b97f81568f5f83a900283cfd35 GIT binary patch literal 1393 zcmV-%1&;cOP)j!rRY~Mas&Yxp6R>^)>?Z&}f#C^Qo&bEJipwr=-~g2O z!dc=8Fel8Z>DG+aLc97NX-2F5RkgLO)?(HF=^4#f{0@}+}7ri?djt!KSbFwL!2SbBy z+DAK1&*CZBA#5{vUD^Y5mw;>v+Xmg#J+e>O9|GJ4Ay8E{0rr;w`zQo`X%AJsx-C4$ zzBJbKMekz2s++O|=yEimYGluF`{8>WgCUG`#88Fj9L#r~TqPtsFwvRYfrgOtd8pt5 zhU0w3Bb)1cZ$u*j=nLEh<$Ply*&{r%3+;p2)E0pL<9l?NfiNRq979cNwE^1s4?#$B zLTy52pHvn=hujQ=O-xCICslr-k^qSYSIA~UA}q^(xO5w!A2pwXP{bT7^qNaWfbMt< z?NPG{A*e_w{3~492)Ga8{3>{$d{#(d1fUxKcnm@v3LE8I0R}k4U>dH1CV4{^MODxP z5a&06+s?DR4`fDwR6R!FfxE?~6L|szAf?|FAqjAE(}|5$kS_fm2&V9|#)~#afRz4L z;0F0-x^J)+0aE%~gB#>&`pvZnkka2OYi+@I5rArTmZ;)US8(B)2KhgJ5ecBH zA0LFS@Z-r{aRm4vbWOA#K?JY|FU493@CyiC&-2twCH*?i-k@JkT7CaQU2 z0Tv*1O;qS(0iJ=-H4FX`383471qfZSN*U0-3I2mW>HJ)*?Xn2~I&U z%|@R}x|(jNmaax~1cE6tYm`g@bcQDxBWli!d^yWHSte=UP_v_(AcTT_26AV{>0dkn zp)RLIn>-XwP1NBdeS_*e%By-}Bz18+gkUmtvAz%|*pXRu#e|^h{ZhV?^!4UxCw)Dm zFD(6oY>uippQ*e)KsPM%WVFsnnMOBFr79?iPDFYJ0#j@M7^b!WjO3S&II2_fV;Tv- z=!TiMHt}FsL{ElQ>-;p01z=<=!}=Imq^G|KPeT|&kD2drV#4{Y!0iI|dlTm@c#GaC z^Z`^GI?r%>;LWK$=o{GoXb3Qs>$#U;|AgS`ruGJ%&Vud>5s)3D!5y=mgl_N#25t4X0Z#?M6CcCcx|b)oh$d)^s+<5Y((>UYYW&?09|KI3qwByiO>~ z=8^4znjGNiW4h#CCM*P%0f3J@RIqf#;c31B`u{%6V<9V~00000NkvXXu0mjf!{2*7 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json new file mode 100644 index 0000000000..2edca40322 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_play_button.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_play_button@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_play_button@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button.png new file mode 100644 index 0000000000000000000000000000000000000000..e265e9c2fd15797e180bb443ee96fc9543b84f49 GIT binary patch literal 647 zcmV;20(kw2P)CMAS-61ID;TU{0YQ5CXo(1Q`-Z*XT4kzu#IR}#82rsfFF z->C$vcMp;pX}L2>#vC^;h7cTfjEhdebilTy+OKA-)kd?~Mkq7|9|)5RwQ;*}DO2P6 zK7|$~5R0y2XDbSioU%0g+Z!@9&d#s9*P-APjt}FQo0*0p+y}zk2jWn`s5fwPmqPkE z3&UtY0ds2S1UBJ;V!495$25wi3KZDj1eV|tb|d|gH63y3MuZd4;S--<-?&OVAeVnf zlSYgLIw#<6p^zY#e>2^XNFq3f0Wukw==%)M13Vmjb%E!Ib($+AsB;1}SUVigEQaTC zu(yr5S?RgKoC!a?CJ-R2cd6AexmG(Va{{lVBPbG!u2AqbGYJI>oIr+@fXjZsHXrP7 zVNqXz2QuTlF4KDOM1_-nPfrE{5cFH8R_{=&J(A?uu9jn-@pxz{+WjAMIqsH(we=nr zx1G2n-;(`7j9^pRD~Zu<%LnL0Rk%H-3F~O)N5|xMsIN7`aw93+Vc3E7e(%>HiTFL9 hZ^GI|>G0ur{sY$mK3ZQr=W74}002ovPDHLkV1l5+6Kwzh literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7e60675901e423dc57822778a52a345cef3e2c40 GIT binary patch literal 1149 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 literal 0 HcmV?d00001 diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 922dae9fa2..a96e4c1af0 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -131,6 +131,8 @@ internal enum Asset { internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") + internal static let voiceMessagePauseButton = ImageAsset(name: "voice_message_pause_button") + internal static let voiceMessagePlayButton = ImageAsset(name: "voice_message_play_button") internal static let voiceMessageRecordButtonDefault = ImageAsset(name: "voice_message_record_button_default") internal static let voiceMessageRecordButtonRecording = ImageAsset(name: "voice_message_record_button_recording") internal static let voiceMessageRecordIcon = ImageAsset(name: "voice_message_record_icon") diff --git a/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m b/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m index b853631cd8..4dfbe686f6 100644 --- a/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m +++ b/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m @@ -112,6 +112,9 @@ - (UIImage*)attachmentIcon: (MXKAttachmentType)type case MXKAttachmentTypeAudio: image = [UIImage imageNamed:@"file_music_icon"]; break; + case MXKAttachmentTypeVoiceMessage: + image = [UIImage imageNamed:@"file_music_icon"]; + break; case MXKAttachmentTypeVideo: image = [UIImage imageNamed:@"file_video_icon"]; break; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 423b84426c..2a216c44f6 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -986,6 +986,9 @@ - (NSString*)accessibilityLabelForAttachmentType:(MXKAttachmentType)attachmentTy case MXKAttachmentTypeAudio: accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_audio", @"Vector", nil); break; + case MXKAttachmentTypeVoiceMessage: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_audio", @"Vector", nil); + break; case MXKAttachmentTypeVideo: accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_video", @"Vector", nil); break; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e37699e73d..e4b2d4433c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -391,6 +391,10 @@ - (void)viewDidLoad [self.bubblesTableView registerNib:RoomTypingBubbleCell.nib forCellReuseIdentifier:RoomTypingBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:VoiceMessageBubbleCell.class forCellReuseIdentifier:VoiceMessageBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:VoiceMessageWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceMessageWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:VoiceMessageWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceMessageWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [self vc_removeBackTitle]; [self setupRemoveJitsiWidgetRemoveView]; @@ -2367,6 +2371,15 @@ - (void)displayRoomPreview:(RoomPreviewData *)previewData { cellViewClass = RoomGroupCallStatusBubbleCell.class; } + else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage) { + if (bubbleData.isPaginationFirstBubble) { + cellViewClass = VoiceMessageWithPaginationTitleBubbleCell.class; + } else if (bubbleData.shouldHideSenderInformation) { + cellViewClass = VoiceMessageWithoutSenderInfoBubbleCell.class; + } else { + cellViewClass = VoiceMessageBubbleCell.class; + } + } else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) @@ -6163,7 +6176,7 @@ - (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view #pragma mark - VoiceMessageControllerDelegate -- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController didRequestPermissionCheckWithCompletion:(void (^)(BOOL))completion +- (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageController *)voiceMessageController { NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; @@ -6173,13 +6186,13 @@ - (void)voiceMessageController:(VoiceMessageController *)voiceMessageController [MXKTools checkAccessForMediaType:AVMediaTypeAudio manualChangeMessage: message showPopUpInViewController:self completionHandler:^(BOOL granted) { - completion(granted); + }]; } - (void)voiceMessageController:(VoiceMessageController *)voiceMessageController didRequestSendForFileAtURL:(NSURL *)url completion:(void (^)(BOOL))completion { - [self.roomDataSource sendAudioFile:url mimeType:@"audio/mp4" success:^(NSString *eventId) { + [self.roomDataSource sendVoiceMessage:url mimeType:@"audio/m4a" success:^(NSString *eventId) { MXLogDebug(@"Success with event id %@", eventId); completion(YES); } failure:^(NSError *error) { diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift new file mode 100644 index 0000000000..eaeaf722be --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift @@ -0,0 +1,52 @@ +// +// 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 + +class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { + + private var playbackView: VoiceMessagePlaybackView! + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + guard let data = cellData as? RoomBubbleCellData else { + return + } + + guard data.attachment.type == MXKAttachmentTypeVoiceMessage else { + fatalError("Invalid attachment type passed to a voice message cell.") + } + + playbackView.attachment = data.attachment + } + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = true + bubbleCellContentView?.showPaginationTitle = false + + guard let contentView = bubbleCellContentView?.innerContentView else { + return + } + + playbackView = VoiceMessagePlaybackView.instanceFromNib() + bubbleCellContentView?.addSubview(playbackView) + + contentView.vc_addSubViewMatchingParent(playbackView) + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.swift new file mode 100644 index 0000000000..b7e516675c --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.swift @@ -0,0 +1,25 @@ +// +// 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 + +class VoiceMessageWithPaginationTitleBubbleCell: VoiceMessageBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/VoiceMessages/AudioPlayer.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.swift similarity index 75% rename from Riot/Modules/Room/VoiceMessages/AudioPlayer.swift rename to Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.swift index a51e13afea..cc091b39d6 100644 --- a/Riot/Modules/Room/VoiceMessages/AudioPlayer.swift +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.swift @@ -15,3 +15,11 @@ // import Foundation + +class VoiceMessageWithoutSenderInfoBubbleCell: VoiceMessageBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift new file mode 100644 index 0000000000..640d54e5e9 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -0,0 +1,191 @@ +// +// 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 + +protocol VoiceMessageAudioPlayerDelegate: AnyObject { + func audioPlayerDidStartLoading(_ audioPlayer: VoiceMessageAudioPlayer) + func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) + + func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) + func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) + + func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError: Error) +} + +enum VoiceMessageAudioPlayerError: Error { + case genericError +} + +class VoiceMessageAudioPlayer: NSObject { + + private var contentURL: URL! + private var playerItem: AVPlayerItem? + private var audioPlayer: AVPlayer? + + private var statusObserver: NSKeyValueObservation? + private var playbackBufferEmptyObserver: NSKeyValueObservation? + private var rateObserver: NSKeyValueObservation? + private var playToEndObsever: NSObjectProtocol? + + weak var delegate: VoiceMessageAudioPlayerDelegate? + + var isPlaying: Bool { + guard let audioPlayer = audioPlayer else { + return false + } + + return (audioPlayer.rate > 0) + } + + var duration: TimeInterval { + guard let item = self.audioPlayer?.currentItem else { + return 0 + } + + return CMTimeGetSeconds(item.duration) + } + + var currentTime: TimeInterval { + guard let audioPlayer = self.audioPlayer else { + return 0.0 + } + + let currentTime = CMTimeGetSeconds(audioPlayer.currentTime()) + + return currentTime.isNaN ? 0.0 : currentTime + } + + private(set) var isStopped = true + + deinit { + removeObservers() + } + + override init() { + audioPlayer = AVPlayer() + } + + func loadContentFromURL(_ url: URL) { + if contentURL == url { + return + } + + removeObservers() + + delegate?.audioPlayerDidStartLoading(self) + + contentURL = url + playerItem = AVPlayerItem(url: contentURL) + audioPlayer = AVPlayer(playerItem: playerItem) + + addObservers() + } + + func play() { + isStopped = false + + let audioSession = AVAudioSession.sharedInstance() + + do { + try audioSession.setCategory(AVAudioSession.Category.playAndRecord, mode: .default, options: .defaultToSpeaker) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + MXLog.error("Could not redirect audio playback to speakers.") + } + + audioPlayer?.play() + } + + func pause() { + audioPlayer?.pause() + } + + func stop() { + isStopped = true + audioPlayer?.pause() + audioPlayer?.seek(to: .zero) + } + + func seekToTime(_ time: TimeInterval) { + audioPlayer?.seek(to: CMTime(seconds: time, preferredTimescale: 60000)) + } + + // MARK: - Private + + private func addObservers() { + guard let audioPlayer = audioPlayer, let playerItem = playerItem else { + return + } + + statusObserver = playerItem.observe(\.status, options: [.old, .new]) { [weak self] item, change in + guard let self = self else { return } + + switch playerItem.status { + case .failed: + self.delegate?.audioPlayer(self, didFailWithError: playerItem.error ?? VoiceMessageAudioPlayerError.genericError) + case .readyToPlay: + self.delegate?.audioPlayerDidFinishLoading(self) + default: + break + } + } + + playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.old, .new]) { [weak self] item, change in + guard let self = self else { return } + + if playerItem.isPlaybackBufferEmpty { + self.delegate?.audioPlayerDidStartLoading(self) + } else { + self.delegate?.audioPlayerDidFinishLoading(self) + } + } + + rateObserver = audioPlayer.observe(\.rate, options: [.old, .new]) { [weak self] player, change in + guard let self = self else { return } + + if audioPlayer.rate == 0.0 { + self.delegate?.audioPlayerDidStopPlaying(self) + } else { + self.delegate?.audioPlayerDidStartPlaying(self) + } + } + + playToEndObsever = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in + guard let self = self else { return } + + self.delegate?.audioPlayerDidFinishPlaying(self) + } + } + + private func removeObservers() { + statusObserver?.invalidate() + playbackBufferEmptyObserver?.invalidate() + rateObserver?.invalidate() + NotificationCenter.default.removeObserver(playToEndObsever as Any) + } +} + +extension VoiceMessageAudioPlayerDelegate { + func audioPlayerDidStartLoading(_ audioPlayer: VoiceMessageAudioPlayer) { + + } + + func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { + + } +} diff --git a/Riot/Modules/Room/VoiceMessages/AudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift similarity index 72% rename from Riot/Modules/Room/VoiceMessages/AudioRecorder.swift rename to Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index 9ba2161f57..4bf56db841 100644 --- a/Riot/Modules/Room/VoiceMessages/AudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -17,17 +17,17 @@ import Foundation import AVFoundation -protocol AudioRecorderDelegate: AnyObject { - func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) - func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) - func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) +protocol VoiceMessageAudioRecorderDelegate: AnyObject { + func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) + func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) + func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) } -enum AudioRecorderError: Error { +enum VoiceMessageAudioRecorderError: Error { case genericError } -class AudioRecorder: NSObject, AVAudioRecorderDelegate { +class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { private var audioRecorder: AVAudioRecorder? @@ -39,7 +39,7 @@ class AudioRecorder: NSObject, AVAudioRecorderDelegate { return audioRecorder?.currentTime ?? 0 } - weak var delegate: AudioRecorderDelegate? + weak var delegate: VoiceMessageAudioRecorderDelegate? func recordWithOuputURL(_ url: URL) { @@ -55,7 +55,7 @@ class AudioRecorder: NSObject, AVAudioRecorderDelegate { audioRecorder?.record() delegate?.audioRecorderDidStartRecording(self) } catch { - delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError) + delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } } @@ -70,12 +70,12 @@ class AudioRecorder: NSObject, AVAudioRecorderDelegate { if success { delegate?.audioRecorderDidFinishRecording(self) } else { - delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError) + delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } } func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { - delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError) + delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 8623daad16..94306a5aed 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -15,20 +15,21 @@ // import Foundation +import AVFoundation @objc public protocol VoiceMessageControllerDelegate: AnyObject { - func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestPermissionCheckWithCompletion: @escaping (Bool) -> Void) + func voiceMessageControllerDidRequestMicrophonePermission(_ voiceMessageController: VoiceMessageController) func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, completion: @escaping (Bool) -> Void) } -public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, AudioRecorderDelegate { +public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate { private let themeService: ThemeService private let _voiceMessageToolbarView: VoiceMessageToolbarView private let timeFormatter: DateFormatter private var displayLink: CADisplayLink! - private var audioRecorder: AudioRecorder? + private var audioRecorder: VoiceMessageAudioRecorder? @objc public weak var delegate: VoiceMessageControllerDelegate? @@ -58,18 +59,17 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - VoiceMessageToolbarViewDelegate func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) { - delegate?.voiceMessageController(self, didRequestPermissionCheckWithCompletion: { [weak self] success in - guard let self = self, success != false else { - return - } - - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString) - - self.audioRecorder = AudioRecorder() - self.audioRecorder?.delegate = self - self.audioRecorder?.recordWithOuputURL(temporaryFileURL) - }) + guard AVAudioSession.sharedInstance().recordPermission == .granted else { + delegate?.voiceMessageControllerDidRequestMicrophonePermission(self) + return + } + + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") + + self.audioRecorder = VoiceMessageAudioRecorder() + self.audioRecorder?.delegate = self + self.audioRecorder?.recordWithOuputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -94,17 +94,17 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - AudioRecorderDelegate - func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) { + func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { _voiceMessageToolbarView.state = .recording self.displayLink.isPaused = false } - func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) { + func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) { _voiceMessageToolbarView.state = .idle displayLink.isPaused = true } - func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) { + func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) { MXLog.error("Failed recording voice message.") _voiceMessageToolbarView.state = .idle displayLink.isPaused = true diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift new file mode 100644 index 0000000000..675f192f00 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -0,0 +1,215 @@ +// +// 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 +import DSWaveformImage + +private enum VoiceMessagePlaybackViewUIState { + case stopped + case playing + case paused + case error +} + +class VoiceMessagePlaybackView: UIView, VoiceMessageAudioPlayerDelegate { + + private let audioPlayer: VoiceMessageAudioPlayer + private var displayLink: CADisplayLink! + private let timeFormatter: DateFormatter + private var waveformView: VoiceMessageWaveformView! + + @IBOutlet private var backgroundView: UIView! + @IBOutlet private var playButton: UIButton! + @IBOutlet private var elapsedTimeLabel: UILabel! + @IBOutlet private var waveformContainerView: UIView! + + private var state: VoiceMessagePlaybackViewUIState = .stopped { + didSet { + updateUI() + displayLink.isPaused = (state != .playing) + } + } + + var attachment: MXKAttachment? { + didSet { + if oldValue?.contentURL == attachment?.contentURL { + return + } + + switch attachment?.eventSentState { + case MXEventSentStateFailed: + state = .error + default: + state = .stopped + loadAttachmentData() + } + } + } + + static func instanceFromNib() -> VoiceMessagePlaybackView { + let nib = UINib(nibName: "VoiceMessagePlaybackView", bundle: nil) + guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { + fatalError("The nib \(nib) expected its root view to be of type \(self)") + } + return view + } + + override func didMoveToWindow() { + if self.window == nil { + audioPlayer.stop() + displayLink.invalidate() + } + } + + required init?(coder: NSCoder) { + audioPlayer = VoiceMessageAudioPlayer() + + timeFormatter = DateFormatter() + timeFormatter.dateFormat = "m:ss" + + super.init(coder: coder) + + audioPlayer.delegate = self + + displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLinkTick)) + displayLink.isPaused = true + displayLink.add(to: .current, forMode: .common) + } + + override func awakeFromNib() { + super.awakeFromNib() + + backgroundView.layer.cornerRadius = 12.0 + + waveformView = VoiceMessageWaveformView(frame: waveformContainerView.bounds) + waveformContainerView.vc_addSubViewMatchingParent(waveformView) + + updateUI() + } + + // MARK: - VoiceMessageAudioPlayerDelegate + + func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + state = .playing + } + + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + state = .paused + } + + func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { + state = .error + MXLog.error("Failed playing voice message with error: \(error)") + } + + func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + audioPlayer.seekToTime(0.0) + state = .stopped + } + + // MARK: - Private + + private func updateUI() { + playButton.isEnabled = (state != .error) + playButton.setImage((state == .playing ? Asset.Images.voiceMessagePauseButton.image : Asset.Images.voiceMessagePlayButton.image), for: .normal) + + switch state { + case .stopped: + elapsedTimeLabel.text = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: 0.0)) + waveformView.progress = 0.0 + default: + elapsedTimeLabel.text = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) + waveformView.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + } + } + + @IBAction private func onPlayButtonTap() { + if audioPlayer.isPlaying { + audioPlayer.pause() + } else { + audioPlayer.play() + } + } + + @objc private func handleDisplayLinkTick() { + updateUI() + } + + private func loadAttachmentData() { + guard let attachment = attachment else { + return + } + + if attachment.isEncrypted { + attachment.decrypt(toTempFile: { [weak self] filePath in + self?.loadFileAtPath(filePath) + }, failure: { [weak self] error in + // A nil error in this case is a cancellation on the MXMediaLoader + if let error = error { + MXLog.error("Failed decrypting attachment with error: \(String(describing: error))") + self?.state = .error + } + }) + } else { + attachment.prepare({ [weak self] in + self?.loadFileAtPath(attachment.cacheFilePath) + }, failure: { [weak self] error in + MXLog.error("Failed preparing attachment with error: \(String(describing: error))") + self?.state = .error + }) + } + } + + private func loadFileAtPath(_ path: String?) { + guard let filePath = path else { + return + } + + let url = URL(fileURLWithPath: filePath) + + // AVPlayer doesn't want to play it otherwise. https://stackoverflow.com/a/9350824 + let newURL = url.appendingPathExtension("m4a") + + do { + try FileManager.default.moveItem(at: url, to: newURL) + } catch { + self.state = .error + MXLog.error("Failed appending voice message extension.") + return + } + + audioPlayer.loadContentFromURL(newURL) + + waveformView.setNeedsLayout() + waveformView.layoutIfNeeded() + + if waveformView.requiredNumberOfSamples == 0 { + return + } + + let analyser = WaveformAnalyzer(audioAssetURL: newURL) + analyser?.samples(count: waveformView.requiredNumberOfSamples, completionHandler: { [weak self] samples in + guard let samples = samples else { + self?.state = .error + return + } + + DispatchQueue.main.async { + self?.waveformView.setSamples(samples) + } + }) + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib new file mode 100644 index 0000000000..72f371dec9 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 70816fefbe..5d7c567be6 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -22,7 +22,7 @@ protocol VoiceMessageToolbarViewDelegate: AnyObject { func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) } -enum VoiceMessageToolbarViewState { +enum VoiceMessageToolbarViewUIState { case idle case recording } @@ -53,7 +53,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel weak var delegate: VoiceMessageToolbarViewDelegate? - var state: VoiceMessageToolbarViewState = .idle { + var state: VoiceMessageToolbarViewUIState = .idle { didSet { switch state { case .recording: diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift new file mode 100644 index 0000000000..3116f30ebb --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -0,0 +1,112 @@ +// +// 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 + +class VoiceMessageWaveformView: UIView { + + private let lineWidth: CGFloat = 2.0 + private let primarylineColor = UIColor.lightGray + private let secondaryLineColor = UIColor.darkGray + private let linePadding: CGFloat = 2.0 + + private var samples: [Float] = [] + private var barViews: [CALayer] = [] + + var progress = 0.0 { + didSet { + updateBarViews() + } + } + + var requiredNumberOfSamples: Int { + return barViews.count + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupBarViews() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func layoutSubviews() { + super.layoutSubviews() + setupBarViews() + } + + func setSamples(_ samples: [Float]) { + self.samples = samples + updateBarViews() + } + + func addSample(_ sample: Float) { + samples.append(sample) + updateBarViews() + } + + // MARK: - Private + + private func setupBarViews() { + for layer in barViews { + layer.removeFromSuperlayer() + } + + var barViews: [CALayer] = [] + + var xOffset: CGFloat = lineWidth / 2 + + while xOffset < bounds.width - lineWidth { + let layer = CALayer() + layer.backgroundColor = primarylineColor.cgColor + layer.cornerRadius = lineWidth / 2 + layer.masksToBounds = true + layer.anchorPoint = CGPoint(x: 0, y: 0.5) + layer.frame = CGRect(x: xOffset, y: bounds.midY - lineWidth / 2, width: lineWidth, height: lineWidth) + + self.layer.addSublayer(layer) + + barViews.append(layer) + + xOffset += lineWidth + linePadding + } + + self.barViews = barViews + + updateBarViews() + } + + private func updateBarViews() { + let drawMappingFactor = bounds.size.height + let minimumGraphAmplitude: CGFloat = lineWidth + + let progressPosition = Int(floor(progress * Double(barViews.count))) + + for (index, layer) in barViews.enumerated() { + let sample = CGFloat(index >= samples.count ? 1 : samples[index]) + + let invertedDbSample = 1 - sample // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) + let drawingAmplitude = max(minimumGraphAmplitude, invertedDbSample * drawMappingFactor) + + layer.frame.origin.y = bounds.midY - drawingAmplitude / 2 + layer.frame.size.height = drawingAmplitude + + layer.backgroundColor = (index < progressPosition ? secondaryLineColor.cgColor : primarylineColor.cgColor) + } + } +} From 1294c273a539e0dd4196ed6743b45a79ca1da622 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 15 Jun 2021 09:53:09 +0300 Subject: [PATCH 008/125] #4090 - Added voice messages feature flag. --- Config/BuildSettings.swift | 4 ++++ .../Room/Views/InputToolbar/RoomInputToolbarView.m | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 3cfcf65dc8..32af3872ae 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -308,6 +308,10 @@ final class BuildSettings: NSObject { static let messageDetailsAllowCopyMedia: Bool = true static let messageDetailsAllowPasteMedia: Bool = true + // MARK: - Voice Message + + static let voiceMessagesEnabled = false + // MARK: - HTTP /// Additional HTTP headers will be sent by all requests. Not recommended to use request-specific headers, like `Authorization`. /// Empty dictionary by default. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 585cde9109..dfbc1b4b0f 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -81,6 +81,10 @@ - (void)awakeFromNib - (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView { + if (BuildSettings.voiceMessagesEnabled == NO) { + return; + } + _voiceMessageToolbarView = voiceMessageToolbarView; self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:self.voiceMessageToolbarView]; @@ -434,6 +438,10 @@ - (void)_updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated { self.actionMenuOpened = NO; + if (BuildSettings.voiceMessagesEnabled == NO) { + return; + } + [UIView animateWithDuration:(animated ? 0.15f : 0.0f) animations:^{ self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; self.voiceMessageToolbarView.alpha = textMessage.length ? 0.0f : 1.0; From 53307f966209705f29cfe9aebd78763891fb5c73 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 15 Jun 2021 10:58:59 +0300 Subject: [PATCH 009/125] #4096 - Voice message playback view theme support. --- .../Contents.json | 23 ++++++++++++++++++ .../voice_message_pause_button_dark.png | Bin 0 -> 691 bytes .../voice_message_pause_button_dark@2x.png | Bin 0 -> 1284 bytes .../voice_message_pause_button_dark@3x.png | Bin 0 -> 1893 bytes .../Contents.json | 23 ++++++++++++++++++ .../voice_message_pause_button_light.png} | Bin .../voice_message_pause_button_light@2x.png} | Bin .../voice_message_pause_button_light@3x.png} | Bin .../voice_message_play_button@3x.png | Bin 1688 -> 0 bytes .../Contents.json | 6 ++--- .../voice_message_play_button_dark.png | Bin 0 -> 847 bytes .../voice_message_play_button_dark@2x.png | Bin 0 -> 1542 bytes .../voice_message_play_button_dark@3x.png | Bin 0 -> 2204 bytes .../Contents.json | 5 ++-- .../voice_message_play_button_light.png} | Bin .../voice_message_play_button_light@2x.png} | Bin Riot/Generated/Images.swift | 6 +++-- .../VoiceMessagePlaybackView.swift | 21 +++++++++++++++- .../VoiceMessagePlaybackView.xib | 4 +-- .../VoiceMessageWaveformView.swift | 5 ++-- 20 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/Contents.json rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_pause_button.imageset/voice_message_pause_button.png => voice_message_pause_button_light.imageset/voice_message_pause_button_light.png} (100%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_pause_button.imageset/voice_message_pause_button@2x.png => voice_message_pause_button_light.imageset/voice_message_pause_button_light@2x.png} (100%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_pause_button.imageset/voice_message_pause_button@3x.png => voice_message_pause_button_light.imageset/voice_message_pause_button_light@3x.png} (100%) delete mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_pause_button.imageset => voice_message_play_button_dark.imageset}/Contents.json (60%) create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark@3x.png rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_play_button.imageset => voice_message_play_button_light.imageset}/Contents.json (62%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_play_button.imageset/voice_message_play_button.png => voice_message_play_button_light.imageset/voice_message_play_button_light.png} (100%) rename Riot/Assets/Images.xcassets/Room/VoiceMessages/{voice_message_play_button.imageset/voice_message_play_button@2x.png => voice_message_play_button_light.imageset/voice_message_play_button_light@2x.png} (100%) diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/Contents.json new file mode 100644 index 0000000000..183d15e303 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_pause_button_dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_pause_button_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_pause_button_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2ded0f9027f743b07e93094dae997febe33ca4a7 GIT binary patch literal 691 zcmV;k0!;mhP) zl-!+fnAu#6v3>C>xu37_YH!!`&Cc%5enW6XFdTNS$KDRWcL?Z#(Us4gY&ZmABHza; z=-cmRGz$9t(8Vron(*??u-n>d?1Jg>0^w%Eg!jj@c<=es{^Z}uQn^E%x1G~5c zgkS(K-t%)Jvep5+YSZ{4!(9{to}8q~y>-zr%OCb%AN7hQSCC!M{=1{z%E)4;_?0hX zH4<|NbkA=@jql}=|8s)8I5+wGVuqRB?JGW#p#M8B3@NLS_vmAibTk8rX@}ZF_HclGfA(KU46&$ Z{4csS?;i<`4P*cS002ovPDHLkV1kL*KP&(M literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_dark.imageset/voice_message_pause_button_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6823be952b44b63b12dbdfb18e58064a03991bcf GIT binary patch literal 1284 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/Contents.json new file mode 100644 index 0000000000..c5f013138f --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_pause_button_light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_pause_button_light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_pause_button_light@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/voice_message_pause_button_light.png similarity index 100% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/voice_message_pause_button_light.png diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/voice_message_pause_button_light@2x.png similarity index 100% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/voice_message_pause_button_light@2x.png diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/voice_message_pause_button_light@3x.png similarity index 100% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button_light.imageset/voice_message_pause_button_light@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png deleted file mode 100644 index 712720487705b5bd238b878ccb3e183596c88176..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1688 zcmV;J250$+P)$n>15!9r!kGe+$`)0+oD1sMh!O!VQeu-P6e)b9Kyrhf zE=qGp14WR0MJyPBodgJA2;S+q?br?#%4`q~~4lozJ)L?YuW{cINF2 zB!w|n=bJ?#7MD4(t^35!K!`22e~JAb$;`~m01`3-Ne~KORD7rn{UayN15GnrchbA#+Q(;qs=I&vbHM2xx3 zvxEGTCt0z%Ea#ETok2#p@@c5x0M$`GVR7k*bI(L31Mn|!6Rz@eYmy3KaXAn=NKI-3 z@PB-ZZ`v@lxh~d`l2kE3DE~Vc{A}=;kXR>)4Ir9a8-_4i+z3A;`GZ6T@C97MWE$KE zy|{0V9RoyHa}S0nIxIof9IFM0#G`6m%`Dh(TSDYrxVSCgYZ&Dx!Gf!^NFrMRbH_h+ zU`Rt`P@Zal3Jx*k4Y{Dkv>}Tq7xWE`^0Pp(b9>qwCbj_MdUAyZ?z&+wOfx_q=IYN2 zn;T$0?1fMlB(8o7hP<#QjTeQsfHJQBKkdJ9`1@05|AWRmxbWI}Xa)u3`Cx7^W`Ht$ z|C2wSqW$B8kzp!_B|XOJsUedTR-O*;ZkD(nlKg% zMq&VSXNOrRm@wr##|O`_yY~=VckfSpk77_PX!;KXgK!HH^3T8jIj!VT=T|&GegVZ9 z8KM}vAd&UED8sJ@Mf20Q5XXRpgV3Ctv68=l!(!-F%t(k z7w6x=jn(TYkbyA(=j_>6@Cnb1DMK!pp#dt(m8JK%@-I6^C;^)cK*jB}ACL=j;q~)a zxxQSezYpPmGJuT=L09@!e$~^|774H`WdJY8N;KcpF+l7!t*Sy@%Nw6wE8E?K(bL`l zH1267Ak9?t+> zC^j`QjFS8GNui5=Xn=hvydqMPK7CTU^usZLhy!$>35AfXPoIQg@$j1OAT82rLJ=hF z(Ga%Yejv`XqF{ouL6#a6^*!>ErUiUpt}}cySXF8bEJx5Oz!Q4hkl$7ejseFF*fGC*>2H!2@9nwdw8{t(GkG zE*Z^4`0e0{v%m26TdD`yFhUD+vw9i%$m|d6q5~AGC~$&!UTo4cG6-(v>M+pFgKlzMc>tZ1RK2tR+!Ck$8Q82rQ;& zG|onxpc^ES3-V*ei$+NG{1_s&0SHrH+F>otmG6uQL diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/Contents.json similarity index 60% rename from Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/Contents.json index 21dd49f04a..f0d8168f91 100644 --- a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "voice_message_pause_button.png", + "filename" : "voice_message_play_button_dark.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "voice_message_pause_button@2x.png", + "filename" : "voice_message_play_button_dark@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "voice_message_pause_button@3x.png", + "filename" : "voice_message_play_button_dark@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..caffa12a8377680fa8e6cc231583c6b0e3771478 GIT binary patch literal 847 zcmV-V1F-ywP)F z7i!%B2o-+68iQ7Tx#2>2Y%H)@my!lNd^}ssbvfhEG`i1V^1_6tTRYLj{kvn8|Bh?c zAv-=1x(_rkjuc=6)9|7RzX>CUI$(CKoOj$^L?+Oc?YMg5FgDb|7n6_XOW7J1klE1W zllf9Z&;CaCJ3oj=*9r2f1A8%1KLQE3&Kno74P&d;lAm4b-SwoY zQ4}H!L*(;Ekrvx=)F($_7~pd++e~`Bw20+bOZdLFhP23adFJz`DL#80x*Il=o5Kq2PTu1Lk#kph+VpICYO z#{9Pm#rKNhqpeH7&@u)g7J@#EW9 z!-jS(E1BM zrgA{2-re=NmgcbstB#JXEQ;l-lKc?=13~zB3i|^V;Z_Tt;xNYG--I`7Cg^ui|z)nVwOLX z2Yiu8TF)fSksB}*RKskkSG#<>NV2)#689{BEa`FHMcy1A}LA%({szy1Qh*GoJ2@55xU(yS@8LVo;W?f z5+s{cBPLC3!F1_Pz>fOl(L4vzV!L&ivZ3iap6lgh5GyB znIB{+pjLW^tu#Ki`4_ggpTkA?Rl>+p!Jz`I4t&v{e#D=D{BG{o)z#s{kGkNmuZIiB zQo*4DeDDQfVc^Rc&+P8*;_+AO_~HBS^!~r7Fj*=%bbyd*95xf%{2SkX^R>CHB)YO4 z&g`2G;5j^}0w1e^mGJn>^-8M2e{Ys;K5&3Xp$`rdkMCgp(Heez_6*5n0#4)c$eX;) z41q3Gh~Iws*;Iq$!-CWe-iU+fzX^-^;`0?0OQ7jZw6?YBtnm_@K&WJs;gv=QTmq|L zd;0}0Hn-p%?FhS39$!%ELclc+V3LW1IV{#6ts=g&13O{crCzFXl>;(3QadOL=pZN? z;bQEp9rUzWqugJpbT@X@4g~Vc@)s|i<7n+*4I)tV!iXE-Ajzt*f~s@?C2`y8;O zbOvDC4fBZ9ntn4FMg`c9?r+-?WF4g<&dJu(|QD%yfXp! zXO|bWn{J^RQCJ)u>ghY{l!B&0bWnW4y|<_;=;`QSZ{B+@Xja(ly4K!AwZKol0OOg0 zr^Q)HJf7foxYElb`=b=KE-*IKJHd^|aAAt%RLA#rw9GB=?}}%7HXML7HV7gBHy{uZ zx^@Yb@X=IR^TFI#ETtN+lq2}5bB;eN%@uHs@?bTPFh+zBx5%LmbJK7|GqN`I;Ar1( ziPy3Qc7%d2Xs05qH2lalt%sWsJraHT;$7uCgBNbpS~`u@%rusj*uEyj%1BG2U&W&i*H07*qoM6N<$f?{dhmjD0& literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button_dark.imageset/voice_message_play_button_dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..404a5f01c17153d2feda8f95ebfb7c3e11f7d4bb GIT binary patch literal 2204 zcmV;N2xIq&P)Zsiv+meZ)f=8fcHLLNRJTi^xWtzP>wiEBXa*6FKs;x9^;t*(%$TbBnjw~cRxCqS|wsH_!fzJn>~lfE3l zdT&{zj2||=?*>d-L}zr&>w}&hpxgE(n1^9U+2Dh{2YQZsJ;i`ZVzmn*s;I_CX2-VY z9oxjQ11inTd5CCa1SVg9>G}Ome#MakIN(+^KcWRT;`m!HKbP(H^I$-#^7+fcRRTjtFby>8PAZ7gN$oPGc7!~o^b}wjI10v~hkCLR#Z*JeZ z*z9L5YXAz_RS=2j%!q)N1-YpMxb#QT<0hDF)nC-q0e+PJix>ZfAHF}WANfm{AZ#eD z-#M%Dsrvg?{f%>-6 zb`3SakJ5kU^mh&Sa_9blr7u3$*RW8Y6IDx%1K!`eFyV*x|9AOv)BBgz{cped8crTR zrk?)`!3UG??O8~#yi?-j0V~t>q!c-lgd+7t3r?Omu4O4`2%e6= z{G7j(<;RSv;!Y?Q3R;$q9X%YV++n2*X$J&Fep(;|Ps;AX&5HTr0c-!^t zH%8?Sl9U5L+yFy}3AqCztX0iXOu`W3XYN2i2~pYjVyN*icR+T)Fs^QC4v#x9K@We0rI>G1)=Ydt0%GgMo4Yi z8Evt$HA%o@Si6@`dT5qE9c@tT- zePw-sTnfvI82{Sz4nfe{8E6NP6pJW=H8rsrzP9Nd!docwRXae877nX85ZD!wMzrZ2 zB1(ltedRB#MQV6#5dy}t+&uB*Q-Lx*?^~UzEZ(lJ5tAg3Ac!;s1k>8|PT)<$m!DT1 zAmXKYDu`Jy98=r$PFR*7{e1aJ5M)Tp%~MYY+W4yYAIYrPJECTps$HU)5y6Kim<7vC zYWx8eY*$0jktpU@eqYlG!TT8?P!2rinie|FK{mDN0Z2dgcqoj|wtZvUvo&WFH1q;V zpxxjx_iSwJndCP8Gg~IiPpBNAO8-p#y@qbk%5?Q};PEgePYF3+nH+V?t+xeR=bON# zKeKJ??e+H?Y5)ggH9$549+OC{hWnMy^CN7=XPT9MrsY7u?B0cQs=b7QBN@=RE#hQ$fhrltu;88Z40iQD5tkuj?bm;cv{OGn$fzA%8} zf)H|N*lGhyj+8RfkKQ-K^_2m-??`9O-X{%06M)fv2tFK;AE;PT>e%n{NelyHd` zr1)v{l^DA~m}lsf_tnG{A|+L6U5khb-eOH@b%N?-irK<9HsuzqJbh0000 + + + + + + - + - - - - - - - - - + - - + - - - - - - + - - - + + @@ -68,6 +72,7 @@ + @@ -75,5 +80,6 @@ + diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 8f7b664f9d..d46493b81f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -20,16 +20,27 @@ protocol VoiceMessageToolbarViewDelegate: AnyObject { func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) } enum VoiceMessageToolbarViewUIState { case idle - case recording + case record + case lockedModeRecord + case lockedModePlayback +} + +struct VoiceMessageToolbarViewDetails { + var state: VoiceMessageToolbarViewUIState = .idle + var elapsedTime: String = "" + var audioSamples: [Float] = [] } class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { @IBOutlet private var backgroundView: UIView! + @IBOutlet private var recordingContainerView: UIView! + @IBOutlet private var recordButtonsContainerView: UIView! @IBOutlet private var primaryRecordButton: UIButton! @IBOutlet private var secondaryRecordButton: UIButton! @@ -37,43 +48,40 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel @IBOutlet private var recordingChromeContainerView: UIView! @IBOutlet private var recordingIndicatorView: UIView! + @IBOutlet private var elapsedTimeLabel: UILabel! + @IBOutlet private var slideToCancelContainerView: UIView! @IBOutlet private var slideToCancelLabel: UILabel! @IBOutlet private var slideToCancelChevron: UIImageView! @IBOutlet private var slideToCancelGradient: UIImageView! - @IBOutlet private var elapsedTimeLabel: UILabel! + @IBOutlet private var lockContainerView: UIView! + @IBOutlet private var lockContainerBackgroundView: UIView! + @IBOutlet private var primaryLockButton: UIButton! + @IBOutlet private var secondaryLockButton: UIButton! + @IBOutlet private var lockChevron: UIView! + + @IBOutlet private var lockedModeContainerView: UIView! + @IBOutlet private var deleteButton: UIButton! + @IBOutlet private var playbackViewContainerView: UIView! + @IBOutlet private var sendButton: UIButton! + + private var playbackView: VoiceMessagePlaybackView! private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 + private var lockChevronToRecordButtonDistance: CGFloat = 0.0 + private var lockChevronToLockButtonDistance: CGFloat = 0.0 + private var panDirection: UISwipeGestureRecognizer.Direction? + + private var details: VoiceMessageToolbarViewDetails? private var currentTheme: Theme? { didSet { - updateUIAnimated(true) + updateUIWithDetails(details, animated: true) } } weak var delegate: VoiceMessageToolbarViewDelegate? - - var state: VoiceMessageToolbarViewUIState = .idle { - didSet { - switch state { - case .recording: - let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) - cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX - startAnimatingRecordingIndicator() - case .idle: - cancelDrag() - } - - updateUIAnimated(true) - } - } - - var elapsedTime: String? { - didSet { - elapsedTimeLabel.text = elapsedTime - } - } @objc static func instanceFromNib() -> VoiceMessageToolbarView { let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil) @@ -87,6 +95,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel super.awakeFromNib() slideToCancelGradient.image = Asset.Images.voiceMessageCancelGradient.image.withRenderingMode(.alwaysTemplate) + lockContainerBackgroundView.layer.cornerRadius = lockContainerBackgroundView.bounds.width / 2.0 let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) longPressGesture.delegate = self @@ -97,7 +106,52 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel longPressGesture.delegate = self recordButtonsContainerView.addGestureRecognizer(panGesture) - updateUIAnimated(false) + playbackView = VoiceMessagePlaybackView.instanceFromNib() + playbackViewContainerView.vc_addSubViewMatchingParent(playbackView) + + updateUIWithDetails(VoiceMessageToolbarViewDetails(), animated: false) + } + + func configureWithDetails(_ details: VoiceMessageToolbarViewDetails) { + elapsedTimeLabel.text = details.elapsedTime + + UIView.animate(withDuration: 0.25) { + self.updatePlaybackViewWithDetails(details) + } + + if self.details?.state != details.state { + switch details.state { + case .record: + var convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) + cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX + + convertedFrame = self.convert(lockChevron.frame, from: lockContainerView) + lockChevronToRecordButtonDistance = recordButtonsContainerView.frame.midY + convertedFrame.maxY + + lockChevronToLockButtonDistance = lockChevron.frame.minY - primaryLockButton.frame.midY + + startAnimatingRecordingIndicator() + default: + cancelDrag() + } + + if details.state == .lockedModeRecord && self.details?.state == .record { + UIView.animate(withDuration: 0.25) { + self.secondaryLockButton.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) + self.secondaryLockButton.alpha = 0.0 + } completion: { _ in + self.updateUIWithDetails(details, animated: true) + } + } else { + updateUIWithDetails(details, animated: true) + } + } + + self.details = details + } + + func getRequiredNumberOfSamples() -> Int { + return playbackView.getRequiredNumberOfSamples() } // MARK: - Themable @@ -119,8 +173,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel case UIGestureRecognizer.State.began: delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) case UIGestureRecognizer.State.ended: - delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) - case UIGestureRecognizer.State.cancelled: + // delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) default: break @@ -128,17 +181,49 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard self.state == .recording && gestureRecognizer.state == .changed else { + guard details?.state == .record && gestureRecognizer.state == .changed else { return } let translation = gestureRecognizer.translation(in: self) - secondaryRecordButton.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0) - slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0) + if abs(translation.x) <= 20.0 && abs(translation.y) <= 20.0 { + panDirection = nil + } else if panDirection == nil { + if abs(translation.x) >= abs(translation.y) { + panDirection = .left + } else { + panDirection = .up + } + } - if abs(translation.x - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 { - cancelDrag() + if panDirection == .left { + secondaryRecordButton.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0) + slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0) + + if abs(translation.x - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 { + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + } + } else if panDirection == .up { + secondaryRecordButton.transform = CGAffineTransform(translationX: 0.0, y: min(0.0, translation.y)) + + let yTranslation = min(max(translation.y + lockChevronToRecordButtonDistance, -lockChevronToLockButtonDistance), 0.0) + lockChevron.transform = CGAffineTransform(translationX: 0.0, y: yTranslation) + + let transitionPercentage = abs(yTranslation) / lockChevronToLockButtonDistance + + lockChevron.alpha = 1.0 - transitionPercentage + secondaryRecordButton.alpha = 1.0 - transitionPercentage + primaryLockButton.alpha = 1.0 - transitionPercentage + lockContainerBackgroundView.alpha = 1.0 - transitionPercentage + secondaryLockButton.alpha = transitionPercentage + + if transitionPercentage >= 1.0 { + self.delegate?.voiceMessageToolbarViewDidRequestLockedModeRecording(self) + } + + } else { + secondaryRecordButton.transform = CGAffineTransform(translationX: min(0.0, translation.x), y: min(0.0, translation.y)) } } @@ -149,19 +234,42 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } } - private func updateUIAnimated(_ animated: Bool) { - UIView.animate(withDuration: (animated ? 0.25 : 0.0)) { - switch self.state { - case .idle: - self.backgroundView.alpha = 0.0 - self.primaryRecordButton.alpha = 1.0 - self.secondaryRecordButton.alpha = 0.0 - self.recordingChromeContainerView.alpha = 0.0 - case .recording: + private func updateUIWithDetails(_ details: VoiceMessageToolbarViewDetails?, animated: Bool) { + guard let details = details else { + return + } + + UIView.animate(withDuration: (animated ? 0.25 : 0.0), delay: 0.0, options: .beginFromCurrentState) { + switch details.state { + case .record: self.backgroundView.alpha = 1.0 self.primaryRecordButton.alpha = 0.0 self.secondaryRecordButton.alpha = 1.0 self.recordingChromeContainerView.alpha = 1.0 + self.lockContainerView.alpha = 1.0 + self.lockContainerBackgroundView.alpha = 1.0 + self.lockedModeContainerView.alpha = 0.0 + self.recordingContainerView.alpha = 1.0 + case .lockedModeRecord: + self.backgroundView.alpha = 1.0 + self.primaryRecordButton.alpha = 0.0 + self.secondaryRecordButton.alpha = 0.0 + self.recordingChromeContainerView.alpha = 0.0 + self.lockContainerView.alpha = 0.0 + self.lockedModeContainerView.alpha = 1.0 + self.recordingContainerView.alpha = 0.0 + default: + self.backgroundView.alpha = 0.0 + self.primaryRecordButton.alpha = 1.0 + self.secondaryRecordButton.alpha = 0.0 + self.recordingChromeContainerView.alpha = 0.0 + self.lockContainerView.alpha = 0.0 + self.lockContainerBackgroundView.alpha = 1.0 + self.primaryLockButton.alpha = 1.0 + self.secondaryLockButton.alpha = 0.0 + self.lockChevron.alpha = 1.0 + self.lockedModeContainerView.alpha = 0.0 + self.recordingContainerView.alpha = 1.0 } guard let theme = self.currentTheme else { @@ -176,18 +284,30 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel self.slideToCancelChevron.tintColor = theme.textSecondaryColor self.elapsedTimeLabel.textColor = theme.textSecondaryColor } completion: { _ in - switch self.state { + switch details.state { case .idle: self.secondaryRecordButton.transform = .identity self.slideToCancelContainerView.transform = .identity + self.lockChevron.transform = .identity + self.secondaryLockButton.transform = .identity default: break } } } + private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails) { + var playbackViewDetails = VoiceMessagePlaybackViewDetails() + playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord) + playbackViewDetails.currentTime = details.elapsedTime + playbackViewDetails.samples = details.audioSamples + playbackViewDetails.playbackEnabled = true + playbackViewDetails.progress = 0.0 + playbackView.configureWithDetails(playbackViewDetails) + } + private func startAnimatingRecordingIndicator() { - if self.state != .recording { + if self.details?.state != .record { return } @@ -202,4 +322,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } } + + @IBAction private func onTrashButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + } + + @IBAction private func onSendButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index 6218ad96ac..fe548450b9 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -11,121 +11,236 @@ - + - + - - + + - - + + - - + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - + + - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + - - - - - - + + + + + + + + + + + + + + + + + + - + + + + + + diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift index 39c159e04a..8a7a65dffb 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -56,11 +56,6 @@ class VoiceMessageWaveformView: UIView { updateBarViews() } - func addSample(_ sample: Float) { - samples.append(sample) - updateBarViews() - } - // MARK: - Private private func setupBarViews() { From 02da2e2c03aed2ea100b72e8d3d98d9a4b36f244 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 22 Jun 2021 09:59:14 +0300 Subject: [PATCH 013/125] #4096 - Weakify display link targets. --- .../VoiceMessageController.swift | 2 +- .../VoiceMessagePlaybackController.swift | 2 +- .../VoiceMessageToolbarView.swift | 3 +- Riot/Utils/WeakObjectWrapper.swift | 34 +++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 Riot/Utils/WeakObjectWrapper.swift diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index b00d68d82e..faa1da2b48 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -51,7 +51,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, timeFormatter.dateFormat = "m:ss" - displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLinkTick)) + displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick)) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 512c17018d..2ec395bd13 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -49,7 +49,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess audioPlayer.delegate = self playbackView.delegate = self - displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLinkTick)) + displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick)) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index d46493b81f..65da2738ab 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -173,8 +173,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel case UIGestureRecognizer.State.began: delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) case UIGestureRecognizer.State.ended: - // delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) - delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) default: break } diff --git a/Riot/Utils/WeakObjectWrapper.swift b/Riot/Utils/WeakObjectWrapper.swift new file mode 100644 index 0000000000..d862f4861e --- /dev/null +++ b/Riot/Utils/WeakObjectWrapper.swift @@ -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 + +class WeakObjectWrapper: NSObject { + + private weak var wrappedObject: AnyObject? + + init(_ object: AnyObject) { + wrappedObject = object + } + + override func responds(to aSelector: Selector!) -> Bool { + return (wrappedObject?.responds(to: aSelector) ?? false) || super.responds(to: aSelector) + } + + override func forwardingTarget(for aSelector: Selector!) -> Any? { + return wrappedObject + } +} From 959ccc9527a35a5b48d360c026a19e83c1b3ee8f Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 22 Jun 2021 13:19:39 +0300 Subject: [PATCH 014/125] #4094 - Added voice messages locked mode playback. --- .../VoiceMessageAudioPlayer.swift | 20 +-- .../VoiceMessageAudioRecorder.swift | 1 - .../VoiceMessageController.swift | 138 ++++++++++++++++-- .../VoiceMessagePlaybackController.swift | 9 +- .../VoiceMessagePlaybackView.swift | 47 ++++-- .../VoiceMessageToolbarView.swift | 33 ++++- .../VoiceMessages/VoiceMessageToolbarView.xib | 10 +- 7 files changed, 206 insertions(+), 52 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 804afbfe74..9bafef2dac 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -33,7 +33,6 @@ enum VoiceMessageAudioPlayerError: Error { class VoiceMessageAudioPlayer: NSObject { - private var contentURL: URL! private var playerItem: AVPlayerItem? private var audioPlayer: AVPlayer? @@ -44,6 +43,8 @@ class VoiceMessageAudioPlayer: NSObject { weak var delegate: VoiceMessageAudioPlayerDelegate? + private(set) var url: URL? + var isPlaying: Bool { guard let audioPlayer = audioPlayer else { return false @@ -57,7 +58,9 @@ class VoiceMessageAudioPlayer: NSObject { return 0 } - return CMTimeGetSeconds(item.duration) + let duration = CMTimeGetSeconds(item.duration) + + return duration.isNaN ? 0.0 : duration } var currentTime: TimeInterval { @@ -76,21 +79,18 @@ class VoiceMessageAudioPlayer: NSObject { removeObservers() } - override init() { - audioPlayer = AVPlayer() - } - func loadContentFromURL(_ url: URL) { - if contentURL == url { + if self.url == url { return } + self.url = url + removeObservers() delegate?.audioPlayerDidStartLoading(self) - - contentURL = url - playerItem = AVPlayerItem(url: contentURL) + + playerItem = AVPlayerItem(url: url) audioPlayer = AVPlayer(playerItem: playerItem) addObservers() diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index 635e5ea409..7b1f8231a4 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -64,7 +64,6 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { } catch { delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } - } func stopRecording() { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index faa1da2b48..666acde8e0 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -16,13 +16,14 @@ import Foundation import AVFoundation +import DSWaveformImage @objc public protocol VoiceMessageControllerDelegate: AnyObject { func voiceMessageControllerDidRequestMicrophonePermission(_ voiceMessageController: VoiceMessageController) func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, completion: @escaping (Bool) -> Void) } -public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate { +public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate, VoiceMessageAudioPlayerDelegate { private let themeService: ThemeService private let _voiceMessageToolbarView: VoiceMessageToolbarView @@ -31,6 +32,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private var audioRecorder: VoiceMessageAudioRecorder? + private var audioPlayer: VoiceMessageAudioPlayer? + private var waveformAnalyser: WaveformAnalyzer? + private var audioSamples: [Float] = [] private var isInLockedMode: Bool = false @@ -55,7 +59,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) - self._voiceMessageToolbarView.update(theme: self.themeService.theme) + _voiceMessageToolbarView.update(theme: themeService.theme) NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil) updateUI() @@ -72,9 +76,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - self.audioRecorder = VoiceMessageAudioRecorder() - self.audioRecorder?.delegate = self - self.audioRecorder?.recordWithOuputURL(temporaryFileURL) + audioRecorder = VoiceMessageAudioRecorder() + audioRecorder?.delegate = self + audioRecorder?.recordWithOuputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -85,10 +89,17 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, return } - delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url) { [weak self] success in - UINotificationFeedbackGenerator().notificationOccurred( (success ? .success : .error)) - self?.deleteRecordingAtURL(url) + guard isInLockedMode else { + sendRecordingAtURL(url) + return } + + audioPlayer = VoiceMessageAudioPlayer() + audioPlayer?.delegate = self + audioPlayer?.loadContentFromURL(url) + audioSamples = [] + + updateUI() } func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { @@ -96,6 +107,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioRecorder?.stopRecording() deleteRecordingAtURL(audioRecorder?.url) UINotificationFeedbackGenerator().notificationOccurred(.error) + updateUI() } func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) { @@ -103,6 +115,26 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, updateUI() } + func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { + if audioPlayer?.isPlaying ?? false { + audioPlayer?.pause() + } else { + audioPlayer?.play() + } + } + + func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) { + guard let url = audioRecorder?.url else { + MXLog.error("Invalid audio recording URL") + return + } + + sendRecordingAtURL(url) + + isInLockedMode = false + updateUI() + } + // MARK: - AudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { @@ -120,8 +152,36 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, MXLog.error("Failed recording voice message.") } + // MARK: - VoiceMessageAudioPlayerDelegate + + func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + updateUI() + } + + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + updateUI() + } + + func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + audioPlayer.seekToTime(0.0) + updateUI() + } + + func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError: Error) { + updateUI() + + MXLog.error("Failed playing voice message.") + } + // MARK: - Private + private func sendRecordingAtURL(_ url: URL) { + delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url) { [weak self] success in + UINotificationFeedbackGenerator().notificationOccurred( (success ? .success : .error)) + self?.deleteRecordingAtURL(url) + } + } + private func deleteRecordingAtURL(_ url: URL?) { guard let url = url else { return @@ -135,7 +195,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } @objc private func handleThemeDidChange() { - self._voiceMessageToolbarView.update(theme: self.themeService.theme) + _voiceMessageToolbarView.update(theme: themeService.theme) } @objc private func handleDisplayLinkTick() { @@ -143,7 +203,19 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUI() { - displayLink.isPaused = !(audioRecorder?.isRecording ?? false) + + let shouldUpdateFromAudioPlayer = isInLockedMode && !(audioRecorder?.isRecording ?? false) + + guard shouldUpdateFromAudioPlayer else { + updateUIFromAudioRecorder() + return + } + + updateUIFromAudioPlayer() + } + + private func updateUIFromAudioRecorder() { + displayLink.isPaused = !(self.audioRecorder?.isRecording ?? false) let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() @@ -151,15 +223,53 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioSamples = [Float](repeating: 0.0, count: requiredNumberOfSamples) } - if let sample = audioRecorder?.averagePowerForChannelNumber(0) { - audioSamples.append(sample) - audioSamples.remove(at: 0) + let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0 + audioSamples.append(sample) + audioSamples.remove(at: 0) + + var details = VoiceMessageToolbarViewDetails() + details.state = (self.audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.audioRecorder?.currentTime ?? 0.0)) + details.audioSamples = audioSamples + _voiceMessageToolbarView.configureWithDetails(details) + } + + private func updateUIFromAudioPlayer() { + guard let audioPlayer = audioPlayer else { + return + } + + guard let url = audioPlayer.url else { + MXLog.error("Invalid audio player url.") + return + } + + displayLink.isPaused = !audioPlayer.isPlaying + + let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() + if audioSamples.count != requiredNumberOfSamples { + audioSamples = [Float](repeating: 0.0, count: requiredNumberOfSamples) + + waveformAnalyser = WaveformAnalyzer(audioAssetURL: url) + waveformAnalyser?.samples(count: requiredNumberOfSamples, completionHandler: { [weak self] samples in + guard let samples = samples else { + MXLog.error("Could not sample audio recording.") + return + } + + DispatchQueue.main.async { + self?.audioSamples = samples + self?.updateUIFromAudioPlayer() + } + }) } var details = VoiceMessageToolbarViewDetails() details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioRecorder?.currentTime ?? 0.0)) + details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) details.audioSamples = audioSamples + details.isPlaying = audioPlayer.isPlaying + details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) _voiceMessageToolbarView.configureWithDetails(details) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 2ec395bd13..63fedd65ae 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -73,7 +73,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess // MARK: - VoiceMessagePlaybackViewDelegate - func voiceMessagePlaybackViewDidRequestToggle() { + func voiceMessagePlaybackViewDidRequestPlaybackToggle() { if audioPlayer.isPlaying { audioPlayer.pause() } else { @@ -149,8 +149,11 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess attachment.prepare({ [weak self] in self?.loadFileAtPath(attachment.cacheFilePath) }, failure: { [weak self] error in - MXLog.error("Failed preparing attachment with error: \(String(describing: error))") - self?.state = .error + // A nil error in this case is a cancellation on the MXMediaLoader + if let error = error { + MXLog.error("Failed preparing attachment with error: \(String(describing: error))") + self?.state = .error + } }) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index ba0bd2654f..466d11903a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -17,7 +17,7 @@ import Foundation protocol VoiceMessagePlaybackViewDelegate: AnyObject { - func voiceMessagePlaybackViewDidRequestToggle() + func voiceMessagePlaybackViewDidRequestPlaybackToggle() } struct VoiceMessagePlaybackViewDetails { @@ -31,7 +31,7 @@ struct VoiceMessagePlaybackViewDetails { class VoiceMessagePlaybackView: UIView { - private var waveformView: VoiceMessageWaveformView! + private var _waveformView: VoiceMessageWaveformView! @IBOutlet private var backgroundView: UIView! @IBOutlet private var recordingIcon: UIView! @@ -43,6 +43,10 @@ class VoiceMessagePlaybackView: UIView { var details: VoiceMessagePlaybackViewDetails? + var waveformView: UIView { + return _waveformView + } + static func instanceFromNib() -> VoiceMessagePlaybackView { let nib = UINib(nibName: "VoiceMessagePlaybackView", bundle: nil) guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { @@ -58,8 +62,8 @@ class VoiceMessagePlaybackView: UIView { backgroundView.layer.cornerRadius = 12.0 - waveformView = VoiceMessageWaveformView(frame: waveformContainerView.bounds) - waveformContainerView.vc_addSubViewMatchingParent(waveformView) + _waveformView = VoiceMessageWaveformView(frame: waveformContainerView.bounds) + waveformContainerView.vc_addSubViewMatchingParent(_waveformView) } func configureWithDetails(_ details: VoiceMessagePlaybackViewDetails?) { @@ -68,40 +72,51 @@ class VoiceMessagePlaybackView: UIView { } playButton.isEnabled = details.playbackEnabled - playButton.isHidden = details.recording - recordingIcon.isHidden = !details.recording + + UIView.performWithoutAnimation { + // UIStackView doesn't respond well to re-setting hidden states https://openradar.appspot.com/22819594 + if playButton.isHidden != details.recording { + playButton.isHidden = details.recording + } + + // UIStackView doesn't respond well to re-setting hidden states https://openradar.appspot.com/22819594 + if recordingIcon.isHidden != !details.recording { + recordingIcon.isHidden = !details.recording + } + } + elapsedTimeLabel.text = details.currentTime - waveformView.progress = details.progress + _waveformView.progress = details.progress if ThemeService.shared().isCurrentThemeDark() { playButton.setImage((details.playing ? Asset.Images.voiceMessagePauseButtonDark.image : Asset.Images.voiceMessagePlayButtonDark.image), for: .normal) backgroundView.backgroundColor = UIColor(rgb: 0x394049) - waveformView.primarylineColor = ThemeService.shared().theme.colors.quarterlyContent - waveformView.secondaryLineColor = ThemeService.shared().theme.colors.secondaryContent + _waveformView.primarylineColor = ThemeService.shared().theme.colors.quarterlyContent + _waveformView.secondaryLineColor = ThemeService.shared().theme.colors.secondaryContent elapsedTimeLabel.textColor = UIColor(rgb: 0x8E99A4) } else { playButton.setImage((details.playing ? Asset.Images.voiceMessagePauseButtonLight.image : Asset.Images.voiceMessagePlayButtonLight.image), for: .normal) backgroundView.backgroundColor = UIColor(rgb: 0xE3E8F0) - waveformView.primarylineColor = ThemeService.shared().theme.colors.quarterlyContent - waveformView.secondaryLineColor = ThemeService.shared().theme.colors.secondaryContent + _waveformView.primarylineColor = ThemeService.shared().theme.colors.quarterlyContent + _waveformView.secondaryLineColor = ThemeService.shared().theme.colors.secondaryContent elapsedTimeLabel.textColor = UIColor(rgb: 0x737D8C) } - waveformView.setSamples(details.samples) + _waveformView.setSamples(details.samples) self.details = details } func getRequiredNumberOfSamples() -> Int { - waveformView.setNeedsLayout() - waveformView.layoutIfNeeded() - return waveformView.requiredNumberOfSamples + _waveformView.setNeedsLayout() + _waveformView.layoutIfNeeded() + return _waveformView.requiredNumberOfSamples } // MARK: - Private @IBAction private func onPlayButtonTap() { - delegate?.voiceMessagePlaybackViewDidRequestToggle() + delegate?.voiceMessagePlaybackViewDidRequestPlaybackToggle() } @objc private func handleThemeDidChange() { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 65da2738ab..106038fc05 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -21,6 +21,8 @@ protocol VoiceMessageToolbarViewDelegate: AnyObject { func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) } enum VoiceMessageToolbarViewUIState { @@ -34,9 +36,11 @@ struct VoiceMessageToolbarViewDetails { var state: VoiceMessageToolbarViewUIState = .idle var elapsedTime: String = "" var audioSamples: [Float] = [] + var isPlaying: Bool = false + var progress: Double = 0.0 } -class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { +class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate, VoiceMessagePlaybackViewDelegate { @IBOutlet private var backgroundView: UIView! @IBOutlet private var recordingContainerView: UIView! @@ -107,8 +111,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel recordButtonsContainerView.addGestureRecognizer(panGesture) playbackView = VoiceMessagePlaybackView.instanceFromNib() + playbackView.delegate = self playbackViewContainerView.vc_addSubViewMatchingParent(playbackView) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleWaveformTap)) + playbackView.waveformView.addGestureRecognizer(tapGesture) + updateUIWithDetails(VoiceMessageToolbarViewDetails(), animated: false) } @@ -166,6 +174,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel return true } + // MARK: - VoiceMessagePlaybackViewDelegate + + func voiceMessagePlaybackViewDidRequestPlaybackToggle() { + delegate?.voiceMessageToolbarViewDidRequestPlaybackToggle(self) + } + // MARK: - Private @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -249,6 +263,14 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel self.lockContainerBackgroundView.alpha = 1.0 self.lockedModeContainerView.alpha = 0.0 self.recordingContainerView.alpha = 1.0 + case .lockedModePlayback: + self.backgroundView.alpha = 1.0 + self.primaryRecordButton.alpha = 0.0 + self.secondaryRecordButton.alpha = 0.0 + self.recordingChromeContainerView.alpha = 0.0 + self.lockContainerView.alpha = 0.0 + self.lockedModeContainerView.alpha = 1.0 + self.recordingContainerView.alpha = 0.0 case .lockedModeRecord: self.backgroundView.alpha = 1.0 self.primaryRecordButton.alpha = 0.0 @@ -257,7 +279,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel self.lockContainerView.alpha = 0.0 self.lockedModeContainerView.alpha = 1.0 self.recordingContainerView.alpha = 0.0 - default: + case .idle: self.backgroundView.alpha = 0.0 self.primaryRecordButton.alpha = 1.0 self.secondaryRecordButton.alpha = 0.0 @@ -298,10 +320,11 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails) { var playbackViewDetails = VoiceMessagePlaybackViewDetails() playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord) + playbackViewDetails.playing = details.isPlaying + playbackViewDetails.progress = details.progress playbackViewDetails.currentTime = details.elapsedTime playbackViewDetails.samples = details.audioSamples playbackViewDetails.playbackEnabled = true - playbackViewDetails.progress = 0.0 playbackView.configureWithDetails(playbackViewDetails) } @@ -327,6 +350,10 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } @IBAction private func onSendButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestSend(self) + } + + @objc private func handleWaveformTap(_ gestureRecognizer: UITapGestureRecognizer) { delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index fe548450b9..fb56fcd18a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -147,8 +147,8 @@ - - + + - + - + + + + + + + + + + + + + + + + + - - + + + - - - @@ -75,7 +90,7 @@ - + @@ -211,6 +226,7 @@ + @@ -239,8 +255,8 @@ - - + + diff --git a/Riot/Utils/PassthroughView.swift b/Riot/Utils/PassthroughView.swift index 2d89fc8f7f..b7dad91b08 100644 --- a/Riot/Utils/PassthroughView.swift +++ b/Riot/Utils/PassthroughView.swift @@ -16,6 +16,9 @@ import UIKit +/** + UIView subclass that ignores touches on itself. + */ class PassthroughView: UIView { public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let hitTarget = super.hitTest(point, with: event) diff --git a/Riot/Utils/WeakObjectWrapper.swift b/Riot/Utils/WeakObjectWrapper.swift index d862f4861e..a1a206abb2 100644 --- a/Riot/Utils/WeakObjectWrapper.swift +++ b/Riot/Utils/WeakObjectWrapper.swift @@ -16,6 +16,10 @@ import Foundation +/** + Used to avoid retain cycles by creating a proxy that holds a weak reference to the original object. + One example of that would be using CADisplayLink, which strongly retains its target, when manually invalidating it is unfeasable. + */ class WeakObjectWrapper: NSObject { private weak var wrappedObject: AnyObject? From 7d257f58789c4f0ae80015ee46d2556410fc53e5 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 23 Jun 2021 17:52:08 +0300 Subject: [PATCH 017/125] #4094 - Added multiple observation on media services and a mediaServiceProvider that prevents simultaneous playback from multiple player instances. --- Riot/Modules/Room/RoomViewController.m | 2 +- .../VoiceMessage/VoiceMessageBubbleCell.swift | 2 +- .../VoiceMessageAudioPlayer.swift | 69 ++++++++++++---- .../VoiceMessageAudioRecorder.swift | 39 +++++++-- .../VoiceMessageController.swift | 20 +++-- .../VoiceMessageMediaServiceProvider.swift | 80 +++++++++++++++++++ .../VoiceMessagePlaybackController.swift | 13 ++- Riot/Utils/DelegateContainer.swift | 47 +++++++++++ 8 files changed, 237 insertions(+), 35 deletions(-) create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift create mode 100644 Riot/Utils/DelegateContainer.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d2b0b1875e..4d0786654e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -316,7 +316,7 @@ - (void)finalizeInit // Show / hide actions button in document preview according BuildSettings self.allowActionsInDocumentPreview = BuildSettings.messageDetailsAllowShare; - _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared]; + _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider]; self.voiceMessageController.delegate = self; } diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift index 09108d0c97..db7eb1b8f3 100644 --- a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift @@ -44,7 +44,7 @@ class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplaya return } - playbackController = VoiceMessagePlaybackController() + playbackController = VoiceMessagePlaybackController(mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider) bubbleCellContentView?.addSubview(playbackController.playbackView) contentView.vc_addSubViewMatchingParent(playbackController.playbackView) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 9bafef2dac..76f5dfbfcc 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -21,6 +21,7 @@ protocol VoiceMessageAudioPlayerDelegate: AnyObject { func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) + func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) @@ -41,7 +42,7 @@ class VoiceMessageAudioPlayer: NSObject { private var rateObserver: NSKeyValueObservation? private var playToEndObsever: NSObjectProtocol? - weak var delegate: VoiceMessageAudioPlayerDelegate? + private let delegateContainer = DelegateContainer() private(set) var url: URL? @@ -88,8 +89,10 @@ class VoiceMessageAudioPlayer: NSObject { removeObservers() - delegate?.audioPlayerDidStartLoading(self) - + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStartLoading(self) + } + playerItem = AVPlayerItem(url: url) audioPlayer = AVPlayer(playerItem: playerItem) @@ -122,6 +125,14 @@ class VoiceMessageAudioPlayer: NSObject { audioPlayer?.seek(to: CMTime(seconds: time, preferredTimescale: 60000)) } + func registerDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) { + delegateContainer.registerDelegate(delegate) + } + + func deregisterDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) { + delegateContainer.deregisterDelegate(delegate) + } + // MARK: - Private private func addObservers() { @@ -134,9 +145,13 @@ class VoiceMessageAudioPlayer: NSObject { switch playerItem.status { case .failed: - self.delegate?.audioPlayer(self, didFailWithError: playerItem.error ?? VoiceMessageAudioPlayerError.genericError) + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayer(self, didFailWithError: playerItem.error ?? VoiceMessageAudioPlayerError.genericError) + } case .readyToPlay: - self.delegate?.audioPlayerDidFinishLoading(self) + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishLoading(self) + } default: break } @@ -146,9 +161,13 @@ class VoiceMessageAudioPlayer: NSObject { guard let self = self else { return } if playerItem.isPlaybackBufferEmpty { - self.delegate?.audioPlayerDidStartLoading(self) + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStartLoading(self) + } } else { - self.delegate?.audioPlayerDidFinishLoading(self) + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishLoading(self) + } } } @@ -156,16 +175,28 @@ class VoiceMessageAudioPlayer: NSObject { guard let self = self else { return } if audioPlayer.rate == 0.0 { - self.delegate?.audioPlayerDidStopPlaying(self) + if self.isStopped { + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStopPlaying(self) + } + } else { + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidPausePlaying(self) + } + } } else { - self.delegate?.audioPlayerDidStartPlaying(self) + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidStartPlaying(self) + } } } playToEndObsever = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in guard let self = self else { return } - self.delegate?.audioPlayerDidFinishPlaying(self) + self.delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishPlaying(self) + } } } @@ -178,11 +209,17 @@ class VoiceMessageAudioPlayer: NSObject { } extension VoiceMessageAudioPlayerDelegate { - func audioPlayerDidStartLoading(_ audioPlayer: VoiceMessageAudioPlayer) { - - } + func audioPlayerDidStartLoading(_ audioPlayer: VoiceMessageAudioPlayer) { } - func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { - - } + func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { } + + func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { } + + func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { } + + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { } + + func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { } + + func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError: Error) { } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index 4401d11b3c..0ea7a55944 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -34,6 +34,7 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { } private var audioRecorder: AVAudioRecorder? + private let delegateContainer = DelegateContainer() var url: URL? { return audioRecorder?.url @@ -47,8 +48,6 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { return audioRecorder?.isRecording ?? false } - weak var delegate: VoiceMessageAudioRecorderDelegate? - func recordWithOuputURL(_ url: URL) { let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC), @@ -62,9 +61,13 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { audioRecorder?.delegate = self audioRecorder?.isMeteringEnabled = true audioRecorder?.record() - delegate?.audioRecorderDidStartRecording(self) + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorderDidStartRecording(self) + } } catch { - delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) + } } } @@ -92,18 +95,32 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { return self.normalizedPowerLevelFromDecibels(audioRecorder.averagePower(forChannel: channelNumber)) } + func registerDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) { + delegateContainer.registerDelegate(delegate) + } + + func deregisterDelegate(_ delegate: VoiceMessageAudioPlayerDelegate) { + delegateContainer.deregisterDelegate(delegate) + } + // MARK: - AVAudioRecorderDelegate func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully success: Bool) { if success { - delegate?.audioRecorderDidFinishRecording(self) + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorderDidFinishRecording(self) + } } else { - delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) + } } } func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { - delegate?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) + } } private func normalizedPowerLevelFromDecibels(_ decibels: Float) -> Float { @@ -114,3 +131,11 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { extension String: LocalizedError { public var errorDescription: String? { return self } } + +extension VoiceMessageAudioRecorderDelegate { + func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { } + + func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) { } + + func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) { } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index b65d66d15a..142536c376 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -26,6 +26,8 @@ import DSWaveformImage public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate, VoiceMessageAudioPlayerDelegate { private let themeService: ThemeService + private let mediaServiceProvider: VoiceMessageMediaServiceProvider + private let _voiceMessageToolbarView: VoiceMessageToolbarView private let timeFormatter: DateFormatter private var displayLink: CADisplayLink! @@ -44,9 +46,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, return _voiceMessageToolbarView } - @objc public init(themeService: ThemeService) { - _voiceMessageToolbarView = VoiceMessageToolbarView.loadFromNib() + @objc public init(themeService: ThemeService, mediaServiceProvider: VoiceMessageMediaServiceProvider) { self.themeService = themeService + self.mediaServiceProvider = mediaServiceProvider + + _voiceMessageToolbarView = VoiceMessageToolbarView.loadFromNib() self.timeFormatter = DateFormatter() super.init() @@ -76,8 +80,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - audioRecorder = VoiceMessageAudioRecorder() - audioRecorder?.delegate = self + audioRecorder = mediaServiceProvider.audioRecorder() + audioRecorder?.registerDelegate(self) audioRecorder?.recordWithOuputURL(temporaryFileURL) } @@ -94,8 +98,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, return } - audioPlayer = VoiceMessageAudioPlayer() - audioPlayer?.delegate = self + audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer?.registerDelegate(self) audioPlayer?.loadContentFromURL(url) audioSamples = [] @@ -161,6 +165,10 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, updateUI() } + func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + updateUI() + } + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { updateUI() } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift new file mode 100644 index 0000000000..3c192a6793 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -0,0 +1,80 @@ +// +// 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 + +@objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate { + + private let audioPlayers: NSHashTable + private let audioRecorders: NSHashTable + + @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() + + private override init() { + audioPlayers = NSHashTable(options: .weakMemory) + audioRecorders = NSHashTable(options: .weakMemory) + } + + @objc func audioPlayer() -> VoiceMessageAudioPlayer { + let audioPlayer = VoiceMessageAudioPlayer() + audioPlayer.registerDelegate(self) + audioPlayers.add(audioPlayer) + return audioPlayer + } + + @objc func audioRecorder() -> VoiceMessageAudioRecorder { + let audioRecorder = VoiceMessageAudioRecorder() + audioRecorder.registerDelegate(self) + audioRecorders.add(audioRecorder) + return audioRecorder + } + + @objc func stopAllServices() { + stopAllServicesExcept(nil) + } + + // MARK: - VoiceMessageAudioPlayerDelegate + + func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + stopAllServicesExcept(audioPlayer) + } + + // MARK: - VoiceMessageAudioRecorderDelegate + + func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { + stopAllServicesExcept(audioRecorder) + } + + // MARK: - Private + + private func stopAllServicesExcept(_ service: AnyObject?) { + for audioPlayer in audioPlayers.allObjects { + if audioPlayer === service { + continue + } + + audioPlayer.stop() + } + + for audioRecoder in audioRecorders.allObjects { + if audioRecoder === service { + continue + } + + audioRecoder.stopRecording() + } + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index a624c083e5..c89f7a6e16 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -25,6 +25,7 @@ enum VoiceMessagePlaybackControllerState { } class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMessagePlaybackViewDelegate { + private let audioPlayer: VoiceMessageAudioPlayer private let timeFormatter: DateFormatter private var displayLink: CADisplayLink! @@ -39,14 +40,14 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess let playbackView: VoiceMessagePlaybackView - init() { + init(mediaServiceProvider: VoiceMessageMediaServiceProvider) { playbackView = VoiceMessagePlaybackView.loadFromNib() - audioPlayer = VoiceMessageAudioPlayer() + audioPlayer = mediaServiceProvider.audioPlayer() timeFormatter = DateFormatter() timeFormatter.dateFormat = "m:ss" - audioPlayer.delegate = self + audioPlayer.registerDelegate(self) playbackView.delegate = self displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick)) @@ -94,10 +95,14 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess state = .playing } - func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { state = .paused } + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + state = .stopped + } + func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { state = .error MXLog.error("Failed playing voice message with error: \(error)") diff --git a/Riot/Utils/DelegateContainer.swift b/Riot/Utils/DelegateContainer.swift new file mode 100644 index 0000000000..c4323e980a --- /dev/null +++ b/Riot/Utils/DelegateContainer.swift @@ -0,0 +1,47 @@ +// +// 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 + +/** + Object container storing references weakly. Ideal for implementing simple multiple delegation. + */ +struct DelegateContainer { + + private let hashTable: NSHashTable + + var delegates: [AnyObject] { + return hashTable.allObjects + } + + init() { + hashTable = NSHashTable(options: .weakMemory) + } + + func registerDelegate(_ delegate: AnyObject) { + hashTable.add(delegate) + } + + func deregisterDelegate(_ delegate: AnyObject) { + hashTable.remove(delegate) + } + + func notifyDelegatesWithBlock(_ block: (AnyObject) -> Void) { + for delegate in hashTable.allObjects { + block(delegate) + } + } +} From 1b90b2530feeab8d36ce2497a870782920878194 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 24 Jun 2021 11:33:14 +0300 Subject: [PATCH 018/125] #4094 - Optimize expensive date formatters creation. --- .../VoiceMessages/VoiceMessageController.swift | 14 ++++++++------ .../VoiceMessagePlaybackController.swift | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 142536c376..ce96062b2b 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -25,11 +25,16 @@ import DSWaveformImage public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate, VoiceMessageAudioPlayerDelegate { + private static let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "m:ss" + return dateFormatter + }() + private let themeService: ThemeService private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let _voiceMessageToolbarView: VoiceMessageToolbarView - private let timeFormatter: DateFormatter private var displayLink: CADisplayLink! private var audioRecorder: VoiceMessageAudioRecorder? @@ -51,14 +56,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, self.mediaServiceProvider = mediaServiceProvider _voiceMessageToolbarView = VoiceMessageToolbarView.loadFromNib() - self.timeFormatter = DateFormatter() super.init() _voiceMessageToolbarView.delegate = self - timeFormatter.dateFormat = "m:ss" - displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick)) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) @@ -239,7 +241,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, var details = VoiceMessageToolbarViewDetails() details.state = (self.audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.audioRecorder?.currentTime ?? 0.0)) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.audioRecorder?.currentTime ?? 0.0)) details.audioSamples = audioSamples _voiceMessageToolbarView.configureWithDetails(details) } @@ -276,7 +278,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, var details = VoiceMessageToolbarViewDetails() details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) details.audioSamples = audioSamples details.isPlaying = audioPlayer.isPlaying details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index c89f7a6e16..8362040296 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -25,9 +25,14 @@ enum VoiceMessagePlaybackControllerState { } class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMessagePlaybackViewDelegate { + + private static let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "m:ss" + return dateFormatter + }() private let audioPlayer: VoiceMessageAudioPlayer - private let timeFormatter: DateFormatter private var displayLink: CADisplayLink! private var samples: [Float] = [] @@ -43,9 +48,6 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess init(mediaServiceProvider: VoiceMessageMediaServiceProvider) { playbackView = VoiceMessagePlaybackView.loadFromNib() audioPlayer = mediaServiceProvider.audioPlayer() - - timeFormatter = DateFormatter() - timeFormatter.dateFormat = "m:ss" audioPlayer.registerDelegate(self) playbackView.delegate = self @@ -128,10 +130,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess switch state { case .stopped: - details.currentTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.duration)) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.duration)) details.progress = 0.0 default: - details.currentTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) } From aa6064431ee2e3702e9bb2336808accef7495a69 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 24 Jun 2021 12:28:50 +0300 Subject: [PATCH 019/125] #4094 - Added Opus Ogg support through FFmpegKit backed media conversion. --- Config/Project.xcconfig | 2 +- Podfile | 3 +- Riot/Modules/Room/RoomViewController.m | 2 +- .../VoiceMessageAudioConverter.swift | 65 +++++++++++++++++++ .../VoiceMessageController.swift | 21 ++++-- .../VoiceMessagePlaybackController.swift | 34 +++++----- Riot/SupportingFiles/Riot-Bridging-Header.h | 2 + 7 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index 5772467eef..c3c50db549 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -25,7 +25,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 11.0 +IPHONEOS_DEPLOYMENT_TARGET = 12.1 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 SWIFT_VERSION = 5.3.1 diff --git a/Podfile b/Podfile index 0b963c87f6..32d841a903 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '12.1' # Use frameforks to allow usage of pod written in Swift (like PiwikTracker) use_frameworks! @@ -70,6 +70,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' + pod 'ffmpeg-kit-ios-audio', '~> 4.4' pod 'FLEX', '~> 4.4.1', :configurations => ['Debug'] diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 4d0786654e..5d23e8fa60 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6187,7 +6187,7 @@ - (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageContro - (void)voiceMessageController:(VoiceMessageController *)voiceMessageController didRequestSendForFileAtURL:(NSURL *)url completion:(void (^)(BOOL))completion { - [self.roomDataSource sendVoiceMessage:url mimeType:@"audio/m4a" success:^(NSString *eventId) { + [self.roomDataSource sendVoiceMessage:url mimeType:nil success:^(NSString *eventId) { MXLogDebug(@"Success with event id %@", eventId); completion(YES); } failure:^(NSError *error) { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift new file mode 100644 index 0000000000..2df5454cb8 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -0,0 +1,65 @@ +// +// 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 + +enum VoiceMessageAudioConverterError: Error { + case generic(String) + case cancelled +} + +struct VoiceMessageAudioConverter { + static func convertToOpusOgg(sourceURL: URL, destinationURL: URL, completion: @escaping (Result) -> Void) { + let command = "-hide_banner -y -i \"\(sourceURL.path)\" -c:a libopus \"\(destinationURL.path)\"" + executeCommand(command, completion: completion) + } + + static func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL, completion: @escaping (Result) -> Void) { + let command = "-hide_banner -y -i \"\(sourceURL.path)\" -c:a aac_at \"\(destinationURL.path)\"" + executeCommand(command, completion: completion) + } + + static private func executeCommand(_ command: String, completion: @escaping (Result) -> Void) { + FFmpegKitConfig.setLogLevel(0) + + FFmpegKit.executeAsync(command) { session in + guard let session = session else { + completion(.failure(.generic("Invalid session"))) + return + } + + guard let returnCode = session.getReturnCode() else { + completion(.failure(.generic("Invalid return code"))) + return + } + + DispatchQueue.main.async { + if returnCode.isSuccess() { + completion(.success(())) + } else if returnCode.isCancel() { + completion(.failure(.cancelled)) + } else { + completion(.failure(.generic(String(returnCode.getValue())))) + MXLog.error(""" + Failed converting voice message with state: \(String(describing: FFmpegKitConfig.sessionState(toString: session.getState()))), \ + returnCode: \(String(describing: returnCode)), \ + stackTrace: \(String(describing: session.getFailStackTrace())) + """) + } + } + } + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index ce96062b2b..d3e044ce35 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -188,10 +188,23 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private - private func sendRecordingAtURL(_ url: URL) { - delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url) { [weak self] success in - UINotificationFeedbackGenerator().notificationOccurred( (success ? .success : .error)) - self?.deleteRecordingAtURL(url) + private func sendRecordingAtURL(_ sourceURL: URL) { + + let destinationURL = sourceURL.deletingPathExtension().appendingPathExtension("opus") + + VoiceMessageAudioConverter.convertToOpusOgg(sourceURL: sourceURL, destinationURL: destinationURL) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + self.delegate?.voiceMessageController(self, didRequestSendForFileAtURL: destinationURL) { [weak self] success in + UINotificationFeedbackGenerator().notificationOccurred((success ? .success : .error)) + self?.deleteRecordingAtURL(sourceURL) + self?.deleteRecordingAtURL(destinationURL) + } + case .failure(let error): + MXLog.error("Failed failed encoding audio message with: \(error)") + } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 8362040296..dedbd3919e 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -147,7 +147,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess if attachment.isEncrypted { attachment.decrypt(toTempFile: { [weak self] filePath in - self?.loadFileAtPath(filePath) + self?.convertAndLoadFileAtPath(filePath) }, failure: { [weak self] error in // A nil error in this case is a cancellation on the MXMediaLoader if let error = error { @@ -157,7 +157,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess }) } else { attachment.prepare({ [weak self] in - self?.loadFileAtPath(attachment.cacheFilePath) + self?.convertAndLoadFileAtPath(attachment.cacheFilePath) }, failure: { [weak self] error in // A nil error in this case is a cancellation on the MXMediaLoader if let error = error { @@ -168,26 +168,28 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } } - private func loadFileAtPath(_ path: String?) { + private func convertAndLoadFileAtPath(_ path: String?) { guard let filePath = path else { return } - let url = URL(fileURLWithPath: filePath) + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - // AVPlayer doesn't want to play it otherwise. https://stackoverflow.com/a/9350824 - let newURL = url.appendingPathExtension("m4a") - - do { - try? FileManager.default.removeItem(at: newURL) - try FileManager.default.moveItem(at: url, to: newURL) - } catch { - self.state = .error - MXLog.error("Failed appending voice message extension.") - return + VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { [weak self] result in + switch result { + case .success: + self?.loadFileAtURL(newURL) + case .failure(let error): + self?.state = .error + MXLog.error("Failed failed decoding audio message with: \(error)") + } } + } + + private func loadFileAtURL(_ url: URL) { - audioPlayer.loadContentFromURL(newURL) + audioPlayer.loadContentFromURL(url) let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() @@ -195,7 +197,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return } - let analyser = WaveformAnalyzer(audioAssetURL: newURL) + let analyser = WaveformAnalyzer(audioAssetURL: url) analyser?.samples(count: requiredNumberOfSamples, completionHandler: { [weak self] samples in guard let samples = samples else { self?.state = .error diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 3292be7d4b..3333b1805a 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -5,6 +5,8 @@ @import MatrixSDK; @import MatrixKit; +#include + #import "WebViewViewController.h" #import "RiotSplitViewController.h" #import "RiotNavigationController.h" From c12a7dc58237ecc53c49b9811cd47c556aae661e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 24 Jun 2021 14:02:41 +0300 Subject: [PATCH 020/125] #4094 - Added toast notifications and maximum recording duration. --- Riot/Assets/en.lproj/Vector.strings | 6 ++ Riot/Generated/Strings.swift | 12 +++ .../VoiceMessageController.swift | 72 +++++++++++----- .../VoiceMessagePlaybackController.swift | 6 +- .../VoiceMessageToolbarView.swift | 85 ++++++++++--------- .../VoiceMessages/VoiceMessageToolbarView.xib | 22 +++++ 6 files changed, 138 insertions(+), 65 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 234022cf28..b15502a615 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1668,3 +1668,9 @@ Tap the + to start adding people."; "side_menu_action_help" = "Help"; "side_menu_action_feedback" = "Feedback"; "side_menu_app_version" = "Version %@"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Release to send"; +"voice_message_remaining_recording_time" = "%@s left"; +"voice_message_stop_locked_mode_recording" = "Tap on the wavelenghth to stop and playback"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ba05c0233b..ddf58c6bb8 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4838,6 +4838,18 @@ internal enum VectorL10n { internal static var voice: String { return VectorL10n.tr("Vector", "voice") } + /// Release to send + internal static var voiceMessageReleaseToSend: String { + return VectorL10n.tr("Vector", "voice_message_release_to_send") + } + /// %@s left + internal static func voiceMessageRemainingRecordingTime(_ p1: String) -> String { + return VectorL10n.tr("Vector", "voice_message_remaining_recording_time", p1) + } + /// Tap on the wavelenghth to stop and playback + internal static var voiceMessageStopLockedModeRecording: String { + return VectorL10n.tr("Vector", "voice_message_stop_locked_mode_recording") + } /// Warning internal static var warning: String { return VectorL10n.tr("Vector", "warning") diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index d3e044ce35..f0d8a91586 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -25,9 +25,15 @@ import DSWaveformImage public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate, VoiceMessageAudioPlayerDelegate { + private enum Constants { + static let maximumAudioRecordingDuration: TimeInterval = 120.0 + static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0 + static let elapsedTimeFormat = "m:ss" + } + private static let timeFormatter: DateFormatter = { let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "m:ss" + dateFormatter.dateFormat = Constants.elapsedTimeFormat return dateFormatter }() @@ -88,24 +94,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { - audioRecorder?.stopRecording() - - guard let url = audioRecorder?.url else { - MXLog.error("Invalid audio recording URL") - return - } - - guard isInLockedMode else { - sendRecordingAtURL(url) - return - } - - audioPlayer = mediaServiceProvider.audioPlayer() - audioPlayer?.registerDelegate(self) - audioPlayer?.loadContentFromURL(url) - audioSamples = [] - - updateUI() + finishRecording() } func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { @@ -188,6 +177,27 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private + private func finishRecording() { + audioRecorder?.stopRecording() + + guard let url = audioRecorder?.url else { + MXLog.error("Invalid audio recording URL") + return + } + + guard isInLockedMode else { + sendRecordingAtURL(url) + return + } + + audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer?.registerDelegate(self) + audioPlayer?.loadContentFromURL(url) + audioSamples = [] + + updateUI() + } + private func sendRecordingAtURL(_ sourceURL: URL) { let destinationURL = sourceURL.deletingPathExtension().appendingPathExtension("opus") @@ -240,7 +250,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioRecorder() { - displayLink.isPaused = !(self.audioRecorder?.isRecording ?? false) + let isRecording = audioRecorder?.isRecording ?? false + + displayLink.isPaused = !isRecording let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() @@ -252,10 +264,26 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioSamples.append(sample) audioSamples.remove(at: 0) + let currentTime = audioRecorder?.currentTime ?? 0.0 + + if currentTime >= Constants.maximumAudioRecordingDuration { + finishRecording() + return + } + var details = VoiceMessageToolbarViewDetails() - details.state = (self.audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.audioRecorder?.currentTime ?? 0.0)) + details.state = (isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: currentTime)) details.audioSamples = audioSamples + + if isRecording { + if currentTime >= Constants.maximumAudioRecordingDuration - Constants.maximumAudioRecordingLengthReachedThreshold { + details.toastMessage = VectorL10n.voiceMessageRemainingRecordingTime(String(Constants.maximumAudioRecordingLengthReachedThreshold)) + } else { + details.toastMessage = (isInLockedMode ? VectorL10n.voiceMessageStopLockedModeRecording : VectorL10n.voiceMessageReleaseToSend) + } + } + _voiceMessageToolbarView.configureWithDetails(details) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index dedbd3919e..3a4c59057f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -26,9 +26,13 @@ enum VoiceMessagePlaybackControllerState { class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMessagePlaybackViewDelegate { + private enum Constants { + static let elapsedTimeFormat = "m:ss" + } + private static let timeFormatter: DateFormatter = { let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "m:ss" + dateFormatter.dateFormat = Constants.elapsedTimeFormat return dateFormatter }() diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 1722086225..ccb80f522d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -39,6 +39,7 @@ struct VoiceMessageToolbarViewDetails { var audioSamples: [Float] = [] var isPlaying: Bool = false var progress: Double = 0.0 + var toastMessage: String? } class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGestureRecognizerDelegate, VoiceMessagePlaybackViewDelegate { @@ -48,6 +49,7 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture static let animationDuration: TimeInterval = 0.25 static let lockModeTransitionAnimationDuration: TimeInterval = 0.5 static let panDirectionChangeThreshold: CGFloat = 20.0 + static let toastContainerCornerRadii: CGFloat = 8.0 } @IBOutlet private var backgroundView: UIView! @@ -81,6 +83,9 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture @IBOutlet private var playbackViewContainerView: UIView! @IBOutlet private var sendButton: UIButton! + @IBOutlet private var toastNotificationContainerView: UIView! + @IBOutlet private var toastNotificationLabel: UILabel! + private var playbackView: VoiceMessagePlaybackView! private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 @@ -103,6 +108,7 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture lockContainerBackgroundView.layer.cornerRadius = lockContainerBackgroundView.bounds.width / 2.0 lockButtonsContainerView.layer.cornerRadius = lockButtonsContainerView.bounds.width / 2.0 + toastNotificationContainerView.layer.cornerRadius = Constants.toastContainerCornerRadii let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) longPressGesture.delegate = self @@ -126,9 +132,8 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture func configureWithDetails(_ details: VoiceMessageToolbarViewDetails) { elapsedTimeLabel.text = details.elapsedTime - UIView.animate(withDuration: Constants.animationDuration) { - self.updatePlaybackViewWithDetails(details) - } + self.updateToastNotificationsWithDetails(details) + self.updatePlaybackViewWithDetails(details) if self.details?.state != details.state { switch details.state { @@ -258,44 +263,24 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0), delay: 0.0, options: .beginFromCurrentState) { switch details.state { case .record: - self.backgroundView.alpha = 1.0 - self.primaryRecordButton.alpha = 0.0 - self.secondaryRecordButton.alpha = 1.0 - self.recordingChromeContainerView.alpha = 1.0 - self.lockContainerView.alpha = 1.0 self.lockContainerBackgroundView.alpha = 1.0 - self.lockedModeContainerView.alpha = 0.0 - self.recordingContainerView.alpha = 1.0 - case .lockedModePlayback: - self.backgroundView.alpha = 1.0 - self.primaryRecordButton.alpha = 0.0 - self.secondaryRecordButton.alpha = 0.0 - self.recordingChromeContainerView.alpha = 0.0 - self.lockContainerView.alpha = 0.0 - self.lockedModeContainerView.alpha = 1.0 - self.recordingContainerView.alpha = 0.0 - case .lockedModeRecord: - self.backgroundView.alpha = 1.0 - self.primaryRecordButton.alpha = 0.0 - self.secondaryRecordButton.alpha = 0.0 - self.recordingChromeContainerView.alpha = 0.0 - self.lockContainerView.alpha = 0.0 - self.lockedModeContainerView.alpha = 1.0 - self.recordingContainerView.alpha = 0.0 case .idle: - self.backgroundView.alpha = 0.0 - self.primaryRecordButton.alpha = 1.0 - self.secondaryRecordButton.alpha = 0.0 - self.recordingChromeContainerView.alpha = 0.0 - self.lockContainerView.alpha = 0.0 self.lockContainerBackgroundView.alpha = 1.0 self.primaryLockButton.alpha = 1.0 self.secondaryLockButton.alpha = 0.0 self.lockChevron.alpha = 1.0 - self.lockedModeContainerView.alpha = 0.0 - self.recordingContainerView.alpha = 1.0 + default: + break } + self.backgroundView.alpha = (details.state == .idle ? 0.0 : 1.0) + self.primaryRecordButton.alpha = (details.state == .idle ? 1.0 : 0.0) + self.secondaryRecordButton.alpha = (details.state == .record ? 1.0 : 0.0) + self.recordingChromeContainerView.alpha = (details.state == .record ? 1.0 : 0.0) + self.lockContainerView.alpha = (details.state == .record ? 1.0 : 0.0) + self.lockedModeContainerView.alpha = (details.state == .lockedModePlayback || details.state == .lockedModeRecord ? 1.0 : 0.0) + self.recordingContainerView.alpha = (details.state == .idle || details.state == .record ? 1.0 : 0.0) + guard let theme = self.currentTheme else { return } @@ -311,6 +296,8 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture self.lockContainerBackgroundView.backgroundColor = theme.colors.navigation self.lockButtonsContainerView.backgroundColor = theme.colors.navigation + self.toastNotificationContainerView.backgroundColor = theme.colors.primaryContent + } completion: { _ in switch details.state { case .idle: @@ -324,15 +311,29 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture } } - private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails) { - var playbackViewDetails = VoiceMessagePlaybackViewDetails() - playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord) - playbackViewDetails.playing = details.isPlaying - playbackViewDetails.progress = details.progress - playbackViewDetails.currentTime = details.elapsedTime - playbackViewDetails.samples = details.audioSamples - playbackViewDetails.playbackEnabled = true - playbackView.configureWithDetails(playbackViewDetails) + private func updateToastNotificationsWithDetails(_ details: VoiceMessageToolbarViewDetails, animated: Bool = true) { + let shouldShowNotification = details.state != .idle && details.toastMessage != nil + + if shouldShowNotification { + self.toastNotificationLabel.text = details.toastMessage + } + + UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0)) { + self.toastNotificationContainerView.alpha = (shouldShowNotification ? 1.0 : 0.0) + } + } + + private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails, animated: Bool = true) { + UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0)) { + var playbackViewDetails = VoiceMessagePlaybackViewDetails() + playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord) + playbackViewDetails.playing = details.isPlaying + playbackViewDetails.progress = details.progress + playbackViewDetails.currentTime = details.elapsedTime + playbackViewDetails.samples = details.audioSamples + playbackViewDetails.playbackEnabled = true + self.playbackView.configureWithDetails(playbackViewDetails) + } } private func startAnimatingRecordingIndicator() { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index 15f3641c08..902ce9bf22 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -206,16 +206,36 @@ + + + + + + + + + + + + + + + @@ -245,6 +265,8 @@ + + From acebef7853bfcadd5f77f62a6fb6eb4c8d8f80ef Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Jun 2021 12:09:41 +0300 Subject: [PATCH 021/125] #4094 - Added voice message attachment decryption, transcoding and sampling caching layer. --- Riot/Modules/Room/RoomViewController.m | 2 + .../VoiceMessage/VoiceMessageBubbleCell.swift | 3 +- .../VoiceMessageAttachmentCacheManager.swift | 185 ++++++++++++++++++ .../VoiceMessagePlaybackController.swift | 96 ++------- 4 files changed, 208 insertions(+), 78 deletions(-) create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 5d23e8fa60..c4fab0c29e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -616,6 +616,8 @@ - (void)viewWillDisappear:(BOOL)animated self.roomDataSource.showReadMarker = YES; self.updateRoomReadMarker = NO; isAppeared = NO; + + [VoiceMessageMediaServiceProvider.sharedProvider stopAllServices]; } - (void)viewDidAppear:(BOOL)animated diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift index db7eb1b8f3..dbf36d15eb 100644 --- a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift @@ -44,7 +44,8 @@ class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplaya return } - playbackController = VoiceMessagePlaybackController(mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider) + playbackController = VoiceMessagePlaybackController(mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider, + cacheManager: VoiceMessageAttachmentCacheManager.sharedManager) bubbleCellContentView?.addSubview(playbackController.playbackView) contentView.vc_addSubViewMatchingParent(playbackController.playbackView) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift new file mode 100644 index 0000000000..1054c8232e --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -0,0 +1,185 @@ +// +// 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 +import DSWaveformImage + +enum VoiceMessageAttachmentCacheManagerError: Error { + case invalidEventId + case invalidAttachmentType + case decryptionError(Error) + case preparationError(Error) + case conversionError(Error) + case samplingError +} + +/** + Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array. + */ +private class CompletionWrapper { + let completion: (Result<(URL, [Float]), Error>) -> Void + + init(_ completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { + self.completion = completion + } +} + +class VoiceMessageAttachmentCacheManager { + + static let sharedManager = VoiceMessageAttachmentCacheManager() + + private let workQueue: DispatchQueue + + private var completionCallbacks = [String: [CompletionWrapper]]() + private var samples = [String: [Int: [Float]]]() + private var finalURLs = [String: URL]() + + private init() { + workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) + } + + func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { + workQueue.async { + self.enqueueLoadAttachment(attachment, numberOfSamples: numberOfSamples, completion: completion) + } + } + + func enqueueLoadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { + guard attachment.type == MXKAttachmentTypeVoiceMessage else { + DispatchQueue.main.async { + completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) + } + return + } + + guard let identifier = attachment.eventId else { + DispatchQueue.main.async { + completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId)) + } + return + } + + if let finalURL = finalURLs[identifier], let samples = samples[identifier]?[numberOfSamples] { + DispatchQueue.main.async { + completion(Result.success((finalURL, samples))) + } + return + } + + if var callbacks = completionCallbacks[identifier] { + callbacks.append(CompletionWrapper(completion)) + completionCallbacks[identifier] = callbacks + return + } else { + completionCallbacks[identifier] = [CompletionWrapper(completion)] + } + + func sampleFileAtURL(_ url: URL) { + let analyser = WaveformAnalyzer(audioAssetURL: url) + analyser?.samples(count: numberOfSamples, completionHandler: { samples in + self.workQueue.async { + guard let samples = samples else { + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) + return + } + + if var existingSamples = self.samples[identifier] { + existingSamples[numberOfSamples] = samples + } else { + self.samples[identifier] = [numberOfSamples: samples] + } + + self.invokeSuccessCallbacksForIdentifier(identifier, url: url, samples: samples) + } + }) + } + + if let finalURL = finalURLs[identifier] { + sampleFileAtURL(finalURL) + return + } + + func convertFileAtPath(_ path: String?) { + guard let filePath = path else { + return + } + + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") + + VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { result in + switch result { + case .success: + self.finalURLs[identifier] = newURL + sampleFileAtURL(newURL) + case .failure(let error): + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error)) + MXLog.error("Failed failed decoding audio message with: \(error)") + } + } + } + + if attachment.isEncrypted { + attachment.decrypt(toTempFile: { filePath in + convertFileAtPath(filePath) + }, failure: { error in + // A nil error in this case is a cancellation on the MXMediaLoader + if let error = error { + MXLog.error("Failed decrypting attachment with error: \(String(describing: error))") + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error)) + } + }) + } else { + attachment.prepare({ + convertFileAtPath(attachment.cacheFilePath) + }, failure: { error in + // A nil error in this case is a cancellation on the MXMediaLoader + if let error = error { + MXLog.error("Failed preparing attachment with error: \(String(describing: error))") + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.preparationError(error)) + } + }) + } + } + + private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, samples: [Float]) { + guard let callbacks = completionCallbacks[identifier] else { + return + } + + for wrapper in callbacks { + DispatchQueue.main.async { + wrapper.completion(Result.success((url, samples))) + } + } + + completionCallbacks[identifier] = nil + } + + private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) { + guard let callbacks = completionCallbacks[identifier] else { + return + } + + for wrapper in callbacks { + DispatchQueue.main.async { + wrapper.completion(Result.failure(error)) + } + } + + completionCallbacks[identifier] = nil + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 3a4c59057f..9a438f707a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -35,7 +35,9 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess dateFormatter.dateFormat = Constants.elapsedTimeFormat return dateFormatter }() - + + private let cacheManager: VoiceMessageAttachmentCacheManager + private let audioPlayer: VoiceMessageAudioPlayer private var displayLink: CADisplayLink! private var samples: [Float] = [] @@ -49,7 +51,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess let playbackView: VoiceMessagePlaybackView - init(mediaServiceProvider: VoiceMessageMediaServiceProvider) { + init(mediaServiceProvider: VoiceMessageMediaServiceProvider, + cacheManager: VoiceMessageAttachmentCacheManager) { + self.cacheManager = cacheManager + playbackView = VoiceMessagePlaybackView.loadFromNib() audioPlayer = mediaServiceProvider.audioPlayer() @@ -62,22 +67,12 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: .themeServiceDidChangeTheme, object: nil) updateTheme() + updateUI() } - + var attachment: MXKAttachment? { didSet { - if oldValue?.contentURL == attachment?.contentURL && - oldValue?.eventSentState == attachment?.eventSentState { - return - } - - switch attachment?.eventSentState { - case MXEventSentStateFailed: - state = .error - default: - state = .stopped - loadAttachmentData() - } + loadAttachmentData() } } @@ -143,79 +138,26 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess playbackView.configureWithDetails(details) } - + private func loadAttachmentData() { guard let attachment = attachment else { return } - if attachment.isEncrypted { - attachment.decrypt(toTempFile: { [weak self] filePath in - self?.convertAndLoadFileAtPath(filePath) - }, failure: { [weak self] error in - // A nil error in this case is a cancellation on the MXMediaLoader - if let error = error { - MXLog.error("Failed decrypting attachment with error: \(String(describing: error))") - self?.state = .error - } - }) - } else { - attachment.prepare({ [weak self] in - self?.convertAndLoadFileAtPath(attachment.cacheFilePath) - }, failure: { [weak self] error in - // A nil error in this case is a cancellation on the MXMediaLoader - if let error = error { - MXLog.error("Failed preparing attachment with error: \(String(describing: error))") - self?.state = .error - } - }) - } - } - - private func convertAndLoadFileAtPath(_ path: String?) { - guard let filePath = path else { - return - } - - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") + let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() - VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { [weak self] result in + cacheManager.loadAttachment(attachment, numberOfSamples: requiredNumberOfSamples) { result in switch result { - case .success: - self?.loadFileAtURL(newURL) - case .failure(let error): - self?.state = .error - MXLog.error("Failed failed decoding audio message with: \(error)") + case .success(let result): + self.audioPlayer.loadContentFromURL(result.0) + self.samples = result.1 + self.updateUI() + case .failure: + self.state = .error } } } - private func loadFileAtURL(_ url: URL) { - - audioPlayer.loadContentFromURL(url) - - let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() - - if requiredNumberOfSamples == 0 { - return - } - - let analyser = WaveformAnalyzer(audioAssetURL: url) - analyser?.samples(count: requiredNumberOfSamples, completionHandler: { [weak self] samples in - guard let samples = samples else { - self?.state = .error - return - } - - DispatchQueue.main.async { - self?.samples = samples - self?.updateUI() - } - }) - } - - @objc private func updateTheme() { playbackView.update(theme: ThemeService.shared().theme) } From 24f06a7288e66a2408315048781725d7b75e38fa Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Jun 2021 12:13:43 +0300 Subject: [PATCH 022/125] #4094 - Fixed improper weak display link targets. --- .../VoiceMessageController.swift | 2 +- .../VoiceMessagePlaybackController.swift | 2 +- ...pper.swift => WeakDisplayLinkTarget.swift} | 25 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) rename Riot/Utils/{WeakObjectWrapper.swift => WeakDisplayLinkTarget.swift} (66%) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index f0d8a91586..1b072e4458 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -67,7 +67,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, _voiceMessageToolbarView.delegate = self - displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick)) + displayLink = CADisplayLink(target: WeakDisplayLinkTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakDisplayLinkTarget.triggerSelector) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 9a438f707a..dd95edd7e4 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -61,7 +61,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess audioPlayer.registerDelegate(self) playbackView.delegate = self - displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick)) + displayLink = CADisplayLink(target: WeakDisplayLinkTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakDisplayLinkTarget.triggerSelector) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) diff --git a/Riot/Utils/WeakObjectWrapper.swift b/Riot/Utils/WeakDisplayLinkTarget.swift similarity index 66% rename from Riot/Utils/WeakObjectWrapper.swift rename to Riot/Utils/WeakDisplayLinkTarget.swift index a1a206abb2..5cd2e2eb17 100644 --- a/Riot/Utils/WeakObjectWrapper.swift +++ b/Riot/Utils/WeakDisplayLinkTarget.swift @@ -20,19 +20,18 @@ import Foundation Used to avoid retain cycles by creating a proxy that holds a weak reference to the original object. One example of that would be using CADisplayLink, which strongly retains its target, when manually invalidating it is unfeasable. */ -class WeakObjectWrapper: NSObject { - - private weak var wrappedObject: AnyObject? - - init(_ object: AnyObject) { - wrappedObject = object - } - - override func responds(to aSelector: Selector!) -> Bool { - return (wrappedObject?.responds(to: aSelector) ?? false) || super.responds(to: aSelector) +class WeakDisplayLinkTarget: NSObject { + private(set) weak var target: AnyObject? + let selector: Selector + + static let triggerSelector = #selector(WeakDisplayLinkTarget.handleTick(parameter:)) + + init(_ target: AnyObject, selector: Selector) { + self.target = target + self.selector = selector } - - override func forwardingTarget(for aSelector: Selector!) -> Any? { - return wrappedObject + + @objc private func handleTick(parameter: Any) { + _ = self.target?.perform(self.selector, with: parameter) } } From 329215d17d1bdf92ac1d5965745809e6d7d971b0 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Jun 2021 13:18:40 +0300 Subject: [PATCH 023/125] #4094 - Caching layer work queue fixes and preventing sampling division by 0. --- .../VoiceMessageAttachmentCacheManager.swift | 44 ++++++++++--------- .../VoiceMessageController.swift | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 1054c8232e..f1fd66efd9 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -23,6 +23,7 @@ enum VoiceMessageAttachmentCacheManagerError: Error { case decryptionError(Error) case preparationError(Error) case conversionError(Error) + case invalidNumberOfSamples case samplingError } @@ -52,33 +53,32 @@ class VoiceMessageAttachmentCacheManager { } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { - workQueue.async { - self.enqueueLoadAttachment(attachment, numberOfSamples: numberOfSamples, completion: completion) - } - } - - func enqueueLoadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { - DispatchQueue.main.async { - completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) - } + completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) return } guard let identifier = attachment.eventId else { - DispatchQueue.main.async { - completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId)) - } + completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId)) + return + } + + guard numberOfSamples > 0 else { + completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidNumberOfSamples)) return } if let finalURL = finalURLs[identifier], let samples = samples[identifier]?[numberOfSamples] { - DispatchQueue.main.async { - completion(Result.success((finalURL, samples))) - } + completion(Result.success((finalURL, samples))) return } + workQueue.async { + self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) + } + } + + private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { if var callbacks = completionCallbacks[identifier] { callbacks.append(CompletionWrapper(completion)) completionCallbacks[identifier] = callbacks @@ -160,13 +160,14 @@ class VoiceMessageAttachmentCacheManager { return } - for wrapper in callbacks { - DispatchQueue.main.async { + let copy = callbacks.map { $0 } + DispatchQueue.main.async { + for wrapper in copy { wrapper.completion(Result.success((url, samples))) } } - completionCallbacks[identifier] = nil + self.completionCallbacks[identifier] = nil } private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) { @@ -174,12 +175,13 @@ class VoiceMessageAttachmentCacheManager { return } - for wrapper in callbacks { - DispatchQueue.main.async { + let copy = callbacks.map { $0 } + DispatchQueue.main.async { + for wrapper in copy { wrapper.completion(Result.failure(error)) } } - completionCallbacks[identifier] = nil + self.completionCallbacks[identifier] = nil } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 1b072e4458..3f8a61a124 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -300,7 +300,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, displayLink.isPaused = !audioPlayer.isPlaying let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() - if audioSamples.count != requiredNumberOfSamples { + if audioSamples.count != requiredNumberOfSamples && requiredNumberOfSamples > 0 { padSamplesArrayToSize(requiredNumberOfSamples) waveformAnalyser = WaveformAnalyzer(audioAssetURL: url) From 2c80e610253be5614a08797f3b8617f1767ab08d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Jun 2021 14:42:52 +0300 Subject: [PATCH 024/125] #4094 - Fixed missing ffmpegkit module on release builds. Disabled cache manager work queue for now as it's still not working properly. --- .../VoiceMessageAttachmentCacheManager.swift | 16 +++++++++------- .../VoiceMessageAudioConverter.swift | 1 + Riot/SupportingFiles/Riot-Bridging-Header.h | 2 -- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index f1fd66efd9..3546941721 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -42,15 +42,15 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() - private let workQueue: DispatchQueue +// private let workQueue: DispatchQueue private var completionCallbacks = [String: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var finalURLs = [String: URL]() - private init() { - workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) - } +// private init() { +// workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) +// } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { @@ -73,9 +73,9 @@ class VoiceMessageAttachmentCacheManager { return } - workQueue.async { +// workQueue.async { self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) - } +// } } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { @@ -90,7 +90,9 @@ class VoiceMessageAttachmentCacheManager { func sampleFileAtURL(_ url: URL) { let analyser = WaveformAnalyzer(audioAssetURL: url) analyser?.samples(count: numberOfSamples, completionHandler: { samples in - self.workQueue.async { + // Dispatch back from the WaveformAnalyzer's internal queue + DispatchQueue.main.async { +// self.workQueue.async { guard let samples = samples else { self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) return diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 2df5454cb8..87e932b607 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -15,6 +15,7 @@ // import Foundation +import ffmpegkit enum VoiceMessageAudioConverterError: Error { case generic(String) diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 3333b1805a..3292be7d4b 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -5,8 +5,6 @@ @import MatrixSDK; @import MatrixKit; -#include - #import "WebViewViewController.h" #import "RiotSplitViewController.h" #import "RiotNavigationController.h" From fb87fe04589ea93b13e3f1d442c74c8a38dbb06a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Jun 2021 15:46:48 +0300 Subject: [PATCH 025/125] #4094 - Fixed toast notifications background color on dark themes. --- Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift | 2 -- Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index ccb80f522d..b373aaccbb 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -296,8 +296,6 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture self.lockContainerBackgroundView.backgroundColor = theme.colors.navigation self.lockButtonsContainerView.backgroundColor = theme.colors.navigation - self.toastNotificationContainerView.backgroundColor = theme.colors.primaryContent - } completion: { _ in switch details.state { case .idle: diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index 902ce9bf22..f11674470f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -216,7 +216,7 @@ - + From e0cbe499d599b1027fd1684143b42dd6288ca53e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Jun 2021 16:51:45 +0300 Subject: [PATCH 026/125] #4094 - Increased minimum long press duration to 1 second, reversed audio recorder waveform direction, added a minimum recording duration of 5 seconds for hold&send. --- .../Room/VoiceMessages/VoiceMessageController.swift | 9 ++++++--- .../Room/VoiceMessages/VoiceMessageToolbarView.swift | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 3f8a61a124..9c2d500e5f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -29,6 +29,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, static let maximumAudioRecordingDuration: TimeInterval = 120.0 static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0 static let elapsedTimeFormat = "m:ss" + static let minimumRecordingDuration = 5.0 } private static let timeFormatter: DateFormatter = { @@ -186,7 +187,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } guard isInLockedMode else { - sendRecordingAtURL(url) + if audioRecorder?.currentTime ?? 0 >= Constants.minimumRecordingDuration { + sendRecordingAtURL(url) + } return } @@ -261,8 +264,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0 - audioSamples.append(sample) - audioSamples.remove(at: 0) + audioSamples.insert(sample, at: 0) + audioSamples.removeLast() let currentTime = audioRecorder?.currentTime ?? 0.0 diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index b373aaccbb..9a37e99179 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -45,7 +45,7 @@ struct VoiceMessageToolbarViewDetails { class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGestureRecognizerDelegate, VoiceMessagePlaybackViewDelegate { private enum Constants { - static let longPressMinimumDuration: TimeInterval = 0.1 + static let longPressMinimumDuration: TimeInterval = 1.0 static let animationDuration: TimeInterval = 0.25 static let lockModeTransitionAnimationDuration: TimeInterval = 0.5 static let panDirectionChangeThreshold: CGFloat = 20.0 From e8a3084b62ab11472d518e6310874921c2e6e43f Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 29 Jun 2021 08:52:21 +0200 Subject: [PATCH 027/125] #4090 - Use a dedicated dispatch queue for process --- .../VoiceMessageAttachmentCacheManager.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 3546941721..54fc6d9fc2 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -42,15 +42,15 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() -// private let workQueue: DispatchQueue + private let workQueue: DispatchQueue private var completionCallbacks = [String: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var finalURLs = [String: URL]() -// private init() { -// workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) -// } + private init() { + workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) + } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { @@ -73,9 +73,9 @@ class VoiceMessageAttachmentCacheManager { return } -// workQueue.async { + workQueue.async { self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) -// } + } } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { @@ -92,7 +92,6 @@ class VoiceMessageAttachmentCacheManager { analyser?.samples(count: numberOfSamples, completionHandler: { samples in // Dispatch back from the WaveformAnalyzer's internal queue DispatchQueue.main.async { -// self.workQueue.async { guard let samples = samples else { self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) return From 78cb2b82392e6d1a8f170b8d6125373b1b711f22 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 29 Jun 2021 10:17:15 +0200 Subject: [PATCH 028/125] #4090 - Fixed UI regression if `BuildSettings.voiceMessagesEnabled = false` --- Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h | 2 ++ Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m | 6 ++++++ .../Room/Views/InputToolbar/RoomInputToolbarView.xib | 5 +++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 4c682b0dd5..430daa2bf7 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -58,6 +58,8 @@ typedef enum : NSUInteger @property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainToolbarMinHeightConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainToolbarHeightConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageComposerContainerTrailingConstraint; + @property (weak, nonatomic) IBOutlet UIButton *attachMediaButton; @property (weak, nonatomic) IBOutlet UIImageView *inputTextBackgroundView; diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 65bdfce997..fb0460ad89 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -34,6 +34,7 @@ const CGFloat kActionMenuAttachButtonSpringDamping = .45; const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; +const CGFloat kComposerContainerTrailingPadding = 12; @interface RoomInputToolbarView() { @@ -439,6 +440,11 @@ - (void)updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated self.actionMenuOpened = NO; if (BuildSettings.voiceMessagesEnabled == NO) { + self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; + self.messageComposerContainerTrailingConstraint.constant = (textMessage.length ? self.frame.size.width - self.rightInputToolbarButton.frame.origin.x : 0.0f) + kComposerContainerTrailingPadding; + + [self layoutIfNeeded]; + return; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index 1056cd289b..f0f0c35b38 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -1,9 +1,9 @@ - + - + @@ -150,6 +150,7 @@ + From 397f88c6964b9f2c40631adb63b7c1423c0cc8c1 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 29 Jun 2021 10:36:10 +0200 Subject: [PATCH 029/125] #4090 - Hide voice message button when on action mode --- Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index fb0460ad89..e1223b4c7d 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -407,6 +407,10 @@ - (void)setActionMenuOpened:(BOOL)actionMenuOpened [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; + if (BuildSettings.voiceMessagesEnabled) + { + self.voiceMessageToolbarView.alpha = self->growingTextView.text.length > 0 || actionMenuOpened ? 0 : 1; + } } completion:nil]; [UIView animateWithDuration:kActionMenuComposerHeightAnimationDuration animations:^{ From c421af03b2f58e3e95893054be96d6d770ea70e1 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 29 Jun 2021 15:08:55 +0200 Subject: [PATCH 030/125] #4090 - bug fixing and removed work queue --- .../VoiceMessages/VoiceMessageAttachmentCacheManager.swift | 4 ++-- .../Room/VoiceMessages/VoiceMessageController.swift | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 54fc6d9fc2..b50067638a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -73,9 +73,9 @@ class VoiceMessageAttachmentCacheManager { return } - workQueue.async { +// workQueue.async { self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) - } +// } } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 9c2d500e5f..662800be8e 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -29,7 +29,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, static let maximumAudioRecordingDuration: TimeInterval = 120.0 static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0 static let elapsedTimeFormat = "m:ss" - static let minimumRecordingDuration = 5.0 + static let minimumRecordingDuration = 2.0 } private static let timeFormatter: DateFormatter = { @@ -179,15 +179,16 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private private func finishRecording() { + let recordDuration = audioRecorder?.currentTime audioRecorder?.stopRecording() - + guard let url = audioRecorder?.url else { MXLog.error("Invalid audio recording URL") return } guard isInLockedMode else { - if audioRecorder?.currentTime ?? 0 >= Constants.minimumRecordingDuration { + if recordDuration ?? 0 >= Constants.minimumRecordingDuration { sendRecordingAtURL(url) } return From 760f03d3c3f36e5e5424f860d67434a2f434ce3b Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 29 Jun 2021 17:05:01 +0200 Subject: [PATCH 031/125] #4090 - fixed small theme issues --- DesignKit/Source/Colors.swift | 4 ++++ DesignKit/Variants/Dark/DarkColors.swift | 2 ++ DesignKit/Variants/Light/LightColors.swift | 2 ++ .../upload_icon_dark.png | Bin 823 -> 765 bytes .../upload_icon_dark@2x.png | Bin 1563 -> 1478 bytes .../upload_icon_dark@3x.png | Bin 2232 -> 2136 bytes .../VoiceMessagePlaybackView.swift | 2 +- 7 files changed, 9 insertions(+), 1 deletion(-) diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift index b5dc662616..fc96f36080 100644 --- a/DesignKit/Source/Colors.swift +++ b/DesignKit/Source/Colors.swift @@ -45,6 +45,10 @@ import UIKit /// - Icons var quarterlyContent: UIColor { get } + /// - Text + /// - Icons + var quinaryContent: UIColor { get } + /// Separating line var separator: UIColor { get } diff --git a/DesignKit/Variants/Dark/DarkColors.swift b/DesignKit/Variants/Dark/DarkColors.swift index a1cf2ac546..39a3b0893c 100644 --- a/DesignKit/Variants/Dark/DarkColors.swift +++ b/DesignKit/Variants/Dark/DarkColors.swift @@ -32,6 +32,8 @@ public class DarkColors: Colors { public let quarterlyContent: UIColor = UIColor(rgb: 0x6F7882) + public let quinaryContent: UIColor = UIColor(rgb: 0x394049) + public let separator: UIColor = UIColor(rgb: 0x21262C) public let tile: UIColor = UIColor(rgb: 0x394049) diff --git a/DesignKit/Variants/Light/LightColors.swift b/DesignKit/Variants/Light/LightColors.swift index d8b5e108b9..53a9566e2d 100644 --- a/DesignKit/Variants/Light/LightColors.swift +++ b/DesignKit/Variants/Light/LightColors.swift @@ -32,6 +32,8 @@ public class LightColors: Colors { public let quarterlyContent: UIColor = UIColor(rgb: 0xC1C6CD) + public let quinaryContent: UIColor = UIColor(rgb: 0xE3E8F0) + public let separator: UIColor = UIColor(rgb: 0xE3E8F0) public let tile: UIColor = UIColor(rgb: 0xF3F8FD) diff --git a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark.png b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark.png index cb0316cf93ded219526f03359826fe276832b665..e48bf546b69699b752c20fa8b713147113869155 100644 GIT binary patch delta 691 zcmV;k0!;n42K@z)R)3O7L_t(|0nM1bZWBQe$Nw|_K*A~f2uDFe7lY`rpMX$MAczuI zQ7T9z3M!iiAU2hi%7lO@Cu%FC86MW0~a8KKX%wXTTvLRC5-FkH3%Y2+csdAVVMpc_y2!%MhkK#CCBL$VJ)ij!kf&fo2MRb2D0t#c=#n_$8W5)PNwfOwx>hoxPae=wwr(nd@IZ z9x%{*W^Q5(B7Yg>j(LVRL*`f$a}^7qqa^oqd%b4b-V6~_0ShALx|J@jAlK=EEKJDh z$uaDD9g{=W-u{~nPmo@!#|Xi5fmrZ z7%{nnh`=PPOdtypL5D3_y{W}QWT15XEV#NH+nl76%YRSO`*&}L{=RzaHkMab;Da>7 zC#j_rCgsoXU%qYQZrJH#maZ?~a6iN7&CLA=4-t}vo*`}^Iyel~`HZL__{kz{BO>VQ zqsC0FBSZvNts^!eq)~>&V;e1Tx7kq#V9sJfjE}jRT0%(5=F5v1_x2Be%r$%+K%CvJ z^=qc(Ie*5ELLLi)ioKR6$c<@5USi(pG5fRGfvbbAw2S&NM>n8zv+kG}D#+wVq+0j7 zdH5W}m_+ehN`2vSHAl}F&Quh^9+gTDz3p95$@a~e>?z$RC0e;rb)&K6m~H||g=v&y z%t`Jggrf*lu=<}a1!tPJquXU Z`v)e$2tiXae)0eS002ovPDHLkV1i=IN_qeQ delta 749 zcmVujkwh~@NV@twt9ISwtEo<7VZo-Zfv>c0#IMESA zzyS0RPCTq)A#~60&x2k;(Rh5kR8(FtfJx{F;%yNfEOzoQ>?Gz-Q3+W-B9o$6;|F<6 zwuq5%?g4Z@mw(TMqXbob#s)F}9zN;hj$q4P6?v_D?ZA3KhJm&9kKQ&xBE|&anBWRM zuB|JAs&B{qQGIy7J@)2xWCF}0{AopKs0#vS?WmH8cN4qczm9M@S4 zt)AQ2`F0jeFzdVa^YW~3Pme7Fos7EOB{0eU{;y%ZwitPv{8TSNP4ejBgUZEt`s`&I ztbmwjoWlVO3Y_N{GN#LiY(dmWA`WR87#J|khEAUP3~w0{N6d#;o{z?`;6(ZASsL9t)iv2vAU7mw6lC|LOAp7`785sSr3eiXbFx9 z*+ACTbwRfl=F%K^7;Hv9v$30PN*yXMgoKHGVL%*}${nGmtyY_8{EKK5i#9civ?y;@ z<91tem0{q-@NiIsf*kaT!-+_nn2v&KqExQl>1Kyd$^R_y)Au`xk#rr$9L|M;y%@)! zrah6D=f7s46Dh3PE*a}kWERjw;O*JKsuk(<<VW diff --git a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@2x.png b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@2x.png index b58c78907c54b319abb9faa07e09c97e08f77655..b35cce7f6854b9f381f321dbb516160629fcf881 100644 GIT binary patch delta 1409 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 diff --git a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@3x.png b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@3x.png index b04f2a977beb382725f9ac0cf90d50f1b799e9a1..4c3eb73ea69f2135433e4e43b1cd7669b335b051 100644 GIT binary patch delta 2074 zcmV+#2<7*<5!eurReuNVNkl7lS)OGO)3Cs{Ij=qa*~Z7r$=3e{-=G5&!bda3U%EVn=rpa`V=1!CJYK-_~^ zpaLBJ*vXr>qD+aTxa96~_et*q*tCeXCGVFrZ{C~P8G>b0Hh(tDKxsBWP=<#KVC55` z42BN``WN{}{|46qRR^KY=FeP18~;?w+W~QH`MoPUun0nqscdfgcwU%k6E2gYi^M$B zD@c5pu*Cs?;*qz7(idCH3k!ARl-vm>BJmfc>#X=PF=mkgBB1`>RSKF5+{Oe*U9if=gIQMC)ZeO%Qw`VJ z0TzC`cx?+QlA2&<57j8X&%jNbA@u)u{nm}E%See-1b>sDf>q(k>9?vuaE*f2TpDYe zLc*;5{-NGa6}Hc=SOVz}Y;pVUy@!c=mqZ0y``x4W^*LSj6b2UK;A{6)G4~`v5^;%U zy=s;!%sdZA4<`ICww9APql_(>3E7*)CwjG=fnzX4J5+4U#T6}%9qb2?6 zScfAyJj}2BD0*@9-Iw2v69D^dut9KA`nTi%Dsn` zvwb$0HdGFDW+T#-!!8K(S@Z;s)M*DiH!Pnv)PKUl&!5MocfYM)zX}`CYuWVi zwSTO~!fdv#osU006ie9O-^V{b`UtktFJkNwOewkqOZn_SpZ2`>-)Hu^MpCqNd|!4% zu)7=g=iR<5SNQtKYQFo8qZa0!j)xRZ6@>40e|adF1RHsKn9d=IPRw?@#Y4dYc(ZWi zl5V&7!X-L)m|wcwq7h6?&bh<<((M+FV1Mm~bBB4S^}JxJ)Qn(?X%>!L;}I^)s}W3R zlrO@OYdRVk!QA?lf9RlaS5-dPbQQdI;LBcFwb?7fVFj{RRuqu4Fl(T)B5)ZFD^MOT zDE8C^IIMsJnpR{QLprR0w1v6KD4SrvaL{%YSs*nSlEP0rEBwblJVFwD?bWYi=706q zGprFIKT`{P`j9{XQtSBAE3Y6cRM^)&8{T^Rl3vJPg)O}F^2@mN?st$O^@3@_ufs)T zfP7lN^>%V)kb|PZ?|uIYP6u4i)PKSpqe6AXGTOpn1^ zWXO-!tp-sk|D&RPGKOYL_++sd4?y#$c(b0^vbBnp7+Z_r=1Pk4wJN3v9TsuCF z_=sS}Ee7}%EJafIz3*jLe-Rhzvqbf_E}Cjt+n4^0H{UX42OXry=Z!bN32SM$dIq8P zE%{yBxL<|EqkE+|wOA}!S$~z)frzWi?_FCs{(j#wH$rXFtIWj*AB--(D(X>!>$?nFoc-}!t7jQ+AyyVo4X=JdVhL_&b3yZYfKZ~ zUHQqikp1+6(3HKqaY>3jQ5)T<*uA109t2OQ~B;D`=`?nIr=Cyew!->trNqVuk-iOq^&DNkA+icH0NDM=Lew zf&eZZGYB1EBq5fNXp^bC8xK}VQKWgqX9U)v`<-D2x}%VISFpSH?%yPcXlK1k9Peqd zR%Gvr1WBch+rfm|CueuG74OP%(;tm}OD5`WGPESU{`SbjT+^{OAK zH%k1)#)DbUOk1ZiPBpwM`K%}@k~mDPvt<~NVCnR&ZL(J3*OHx2%c{DWXe&~)w#iUb zi)$XcV5x3Gv9&S@p*m#JJJxdHLZRBNJ81+J!^o!D?*T7_lgYMJMt)&Q-cHi>4I)#) zx-k=ONnIz(&41t?Hoq~p^uF>joojrUrBEGmBUlHqIx~+m8}V`}Qa8C1tQ$F4mg#{q zkWQ!4FD5J?AH39=X-E2Cv>A`3qe9+9>L%BM_2cKic{HO6?Xns2L%1MopBB>Bp@0uE zJ@~t~4QA~Dt2#)8F_mBXYy3-^qBYRi`dJ2nLma*Ym=h^U_i}8INB{r;07*qoM6N<$ Eg1h+fyZ`_I delta 2170 zcmV-=2!;395V#SLReuOgNklb7H;Id*6LC_st*}#*xu8J!>P>Cx4{q1_?bvU_t`*AV4P~ zB!HlE(HjslL@+}D8FfsCBwPjolk)=7?E)uvKk`5t3<5!8j*O0Wu5F9c2EEef~AD2h*Mq` zB%TO!Mk*rled^P-M7i!YQjL`4aQ2`VW0Xw`TPWGzSCQ2Vma3>oOXn#2e6!M;5@u!9 zhZoWs7Jp`CHAb`28WLt@HAgej1T+8_7*betjLS}&3Ijx?MsB58>5!_zxGHpdFf4&05R)rKSyf@&hGwh65KvWPjN(;C zK`IJ!`UYF170pD1fNj<8%_<6WqRU_|Ubqnb?bn~p&JGl>=wR7sJxAAkKB7e*dA&^i zGk>+_!w(MMIyE&F<)6E{x~@F8_r-JTZn*IZ7|0A|?_D~gzbwK`j;^uepRO;;Vy363 z*Btx!=q50fN)_WWVFiHAU?_j2)7LM3?b4--#-_$f72_gdBWmQ>-gjw)su&ZZb6fdK zi-eJyqXCOn5Xmj&KbM4Yv7y_;bQY&7?0+jyizQ+6vuNzKtJNt_izQ(;9qLPYT6BbQ z$k}wLFXd^`5f-BkD@$`asmtk7bA+LYSYetoFXp%KzYEtev^|EJBNiN8jWB!t(jLSG zVZ&d1X`{;o)TjrqvnA-toa>%AOSr5h>JuQ^Y=jwt5TaY~7qFOuN{Le1lxPTQx_>1h z5Xd&#m;#ZCFhDEJ5QtY@9XWCKo3Ps1xCdk8Qp(Fb&5;X^9AQGzw_wp4lz1(|GB|4^OmlLWy(WyGR4?X8i+-@1 z;%Wk==@mqq7IlTUbS!>f5;h}q<6zMefmq6ZC1Ex#>IpkZ_HQi>&@Y;CwrNp6%F`jU zNSKor?Wl)7AWkmbj(CwUPJfG%z|;M%g5+j3g}?N*^%Y(-1xfVN($|(X;p8Z5tgu~A zK7F2>WQsrY_XFxRFcqR@`;JxId*zdpU%jg4$&6huk(b_!#^Z&J8#Wo&l~qInj3;+I zcwp)475nQ%dwWXe^TrAbvklENDnu7ku`iLQT!autu9?^hIMG#B%72$rwUKnXkU3;4 z#t`6hUM0Gis=`dK7)=G$IjpYdfQ7>A zwT`LzFvP&lhb9QfrbeIPbIj+u)^V<$Udd7}WmBV%s8qOn$JT>x4s|7rQ)7Ea$Dpkm zeL$U4HAu17?cuJU4}Wv+%>Z5fFR;{uNUc;AE;}0P;EC-IjH|J%6j(fg&*iS|_t#Y) z)cph|m!0ZOb`+#Zgn*N^O8AQJg9ezU>>=CB*f?tF>D+W7dI%bzsjcl37<=C?j#(cp zD@V~6_VnK!+}8F*thD-QRucf0gaBPHNPBI7_jV7uTyw_$rJ!cKmdo6yiY3N1KW& zp3ibKMbgxtfq#H-3KHJPyVb63>RP3y^@lSQ9r7!x7XP^b;cJ_mn;SH_=dlb@_f}u1 za{XWsqKX76tcc;_98uia2PnLZTlN*wiiEYOj| zsqX5+GWffYg>N$$PwOVB9Ib^_;`7hH?9Jm`kF&{-gdP;o&AVIhh6#v}Sa#cRRmhAw wW}4TAl#;S)$a|6(MU(&GL^8S6sP!WM2Q?&ZYFqo{_5c6?07*qoM6N<$f}iyd$N&HU diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index ff9da941aa..019c73454b 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -95,7 +95,7 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { } playButton.backgroundColor = theme.colors.separator - backgroundView.backgroundColor = theme.colors.tile + backgroundView.backgroundColor = theme.colors.quinaryContent _waveformView.primarylineColor = theme.colors.quarterlyContent _waveformView.secondaryLineColor = theme.colors.secondaryContent elapsedTimeLabel.textColor = theme.colors.tertiaryContent From 05c02c36c026d646b676f2039fde82a7159e5cf6 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Fri, 2 Jul 2021 07:38:56 +0200 Subject: [PATCH 032/125] #4090 - Improved performances --- .../xcshareddata/xcschemes/Riot.xcscheme | 3 +- .../VoiceMessageAttachmentCacheManager.swift | 41 +++--- .../VoiceMessageAudioConverter.swift | 14 ++- .../VoiceMessagePlaybackController.swift | 24 +++- .../VoiceMessagePlaybackView.swift | 16 ++- .../VoiceMessageWaveformView.swift | 117 ++++++++++-------- 6 files changed, 140 insertions(+), 75 deletions(-) diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 84ecb908a5..a9bea1d966 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -4,7 +4,8 @@ version = "1.3"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> ) -> Void + let completion: (Result<(URL, TimeInterval, [Float]), Error>) -> Void - init(_ completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { + init(_ completion: @escaping (Result<(URL, TimeInterval, [Float]), Error>) -> Void) { self.completion = completion } } @@ -46,13 +46,14 @@ class VoiceMessageAttachmentCacheManager { private var completionCallbacks = [String: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() + private var durations = [String: TimeInterval]() private var finalURLs = [String: URL]() private init() { workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) } - func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { + func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, TimeInterval, [Float]), Error>) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) return @@ -68,8 +69,8 @@ class VoiceMessageAttachmentCacheManager { return } - if let finalURL = finalURLs[identifier], let samples = samples[identifier]?[numberOfSamples] { - completion(Result.success((finalURL, samples))) + if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] { + completion(Result.success((finalURL, duration, samples))) return } @@ -78,7 +79,8 @@ class VoiceMessageAttachmentCacheManager { // } } - private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) { + private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, Double, [Float]), Error>) -> Void) { + if var callbacks = completionCallbacks[identifier] { callbacks.append(CompletionWrapper(completion)) completionCallbacks[identifier] = callbacks @@ -87,7 +89,7 @@ class VoiceMessageAttachmentCacheManager { completionCallbacks[identifier] = [CompletionWrapper(completion)] } - func sampleFileAtURL(_ url: URL) { + func sampleFileAtURL(_ url: URL, duration: TimeInterval) { let analyser = WaveformAnalyzer(audioAssetURL: url) analyser?.samples(count: numberOfSamples, completionHandler: { samples in // Dispatch back from the WaveformAnalyzer's internal queue @@ -103,13 +105,13 @@ class VoiceMessageAttachmentCacheManager { self.samples[identifier] = [numberOfSamples: samples] } - self.invokeSuccessCallbacksForIdentifier(identifier, url: url, samples: samples) + self.invokeSuccessCallbacksForIdentifier(identifier, url: url, duration: duration, samples: samples) } }) } - if let finalURL = finalURLs[identifier] { - sampleFileAtURL(finalURL) + if let finalURL = finalURLs[identifier], let duration = durations[identifier] { + sampleFileAtURL(finalURL, duration: duration) return } @@ -125,10 +127,21 @@ class VoiceMessageAttachmentCacheManager { switch result { case .success: self.finalURLs[identifier] = newURL - sampleFileAtURL(newURL) + VoiceMessageAudioConverter.mediaDurationAt(newURL) { result in + switch result { + case .success: + if let duration = try? result.get() { + sampleFileAtURL(newURL, duration: duration) + } else { + MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: Failed to retrieve media duration") + } + case .failure(let error): + MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed getting audio duration with: \(error)") + } + } case .failure(let error): self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error)) - MXLog.error("Failed failed decoding audio message with: \(error)") + MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed decoding audio message with: \(error)") } } } @@ -156,7 +169,7 @@ class VoiceMessageAttachmentCacheManager { } } - private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, samples: [Float]) { + private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) { guard let callbacks = completionCallbacks[identifier] else { return } @@ -164,7 +177,7 @@ class VoiceMessageAttachmentCacheManager { let copy = callbacks.map { $0 } DispatchQueue.main.async { for wrapper in copy { - wrapper.completion(Result.success((url, samples))) + wrapper.completion(Result.success((url, duration, samples))) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 87e932b607..6c8f5cadb2 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -29,10 +29,22 @@ struct VoiceMessageAudioConverter { } static func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL, completion: @escaping (Result) -> Void) { - let command = "-hide_banner -y -i \"\(sourceURL.path)\" -c:a aac_at \"\(destinationURL.path)\"" + let command = "-hide_banner -y -i \"\(sourceURL.path)\" -c:a aac_at -b:a 192k \"\(destinationURL.path)\"" executeCommand(command, completion: completion) } + static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result) -> Void) { + DispatchQueue.global(qos: .userInteractive).async { + let mediaInfoSession = FFprobeKit.getMediaInformation(sourceURL.path) + let mediaInfo = mediaInfoSession?.getMediaInformation() + if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") { + completion(.success(duration)) + } else { + completion(.failure(.generic("Failed to get media duration"))) + } + } + } + static private func executeCommand(_ command: String, completion: @escaping (Result) -> Void) { FFmpegKitConfig.setLogLevel(0) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index dd95edd7e4..f3a4af6032 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -41,6 +41,9 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess private let audioPlayer: VoiceMessageAudioPlayer private var displayLink: CADisplayLink! private var samples: [Float] = [] + private var duration: TimeInterval = 0 + private var urlToLoad: URL? + private var loading: Bool = false private var state: VoiceMessagePlaybackControllerState = .stopped { didSet { @@ -82,6 +85,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess if audioPlayer.isPlaying { audioPlayer.pause() } else { + if let urlToLoad = urlToLoad { + audioPlayer.loadContentFromURL(urlToLoad) + } + urlToLoad = nil audioPlayer.play() } } @@ -129,12 +136,13 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess switch state { case .stopped: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.duration)) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) } + details.loading = self.loading playbackView.configureWithDetails(details) } @@ -144,13 +152,23 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return } + self.loading = true + updateUI() + + // TODO: manage a unique instance of audio player. + if audioPlayer.isPlaying || audioPlayer.currentTime > 0 { + audioPlayer.stop() + } + let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() cacheManager.loadAttachment(attachment, numberOfSamples: requiredNumberOfSamples) { result in switch result { case .success(let result): - self.audioPlayer.loadContentFromURL(result.0) - self.samples = result.1 + self.loading = false + self.urlToLoad = result.0 + self.duration = result.1 + self.samples = result.2 self.updateUI() case .failure: self.state = .error diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index 019c73454b..4eeb012425 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -28,6 +28,7 @@ struct VoiceMessagePlaybackViewDetails { var playing: Bool = false var playbackEnabled = false var recording: Bool = false + var loading: Bool = false } class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { @@ -83,10 +84,17 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { } } - elapsedTimeLabel.text = details.currentTime - _waveformView.progress = details.progress - - _waveformView.setSamples(details.samples) + if details.loading { + elapsedTimeLabel.text = "--:--" + _waveformView.progress = 0 + _waveformView.samples = [] + _waveformView.alpha = 0.3 + } else { + elapsedTimeLabel.text = details.currentTime + _waveformView.progress = details.progress + _waveformView.samples = details.samples + _waveformView.alpha = 1 + } self.details = details diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift index 8a7a65dffb..d180236300 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -20,26 +20,48 @@ class VoiceMessageWaveformView: UIView { private let lineWidth: CGFloat = 2.0 private let linePadding: CGFloat = 2.0 + private let renderingQueue: DispatchQueue = DispatchQueue(label: "io.element.VoiceMessageWaveformView.queue", qos: .userInitiated) - private var samples: [Float] = [] - private var barViews: [CALayer] = [] - - var primarylineColor = UIColor.lightGray - var secondaryLineColor = UIColor.darkGray + var samples: [Float] = [] { + didSet { + computeWaveForm() + } + } + var primarylineColor = UIColor.lightGray { + didSet { + backgroundLayer.strokeColor = primarylineColor.cgColor + backgroundLayer.fillColor = primarylineColor.cgColor + } + } + var secondaryLineColor = UIColor.darkGray { + didSet { + progressLayer.strokeColor = secondaryLineColor.cgColor + progressLayer.fillColor = secondaryLineColor.cgColor + } + } + + private let backgroundLayer = CAShapeLayer() + private let progressLayer = CAShapeLayer() + var progress = 0.0 { didSet { - updateBarViews() + progressLayer.frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.width * CGFloat(self.progress), height: self.bounds.height)) } } var requiredNumberOfSamples: Int { - return barViews.count + return Int(self.bounds.size.width / (lineWidth + linePadding)) } override init(frame: CGRect) { super.init(frame: frame) - setupBarViews() + + setupAndAdd(backgroundLayer, with: primarylineColor) + setupAndAdd(progressLayer, with: secondaryLineColor) + progressLayer.masksToBounds = true + + computeWaveForm() } required init?(coder: NSCoder) { @@ -48,61 +70,52 @@ class VoiceMessageWaveformView: UIView { override func layoutSubviews() { super.layoutSubviews() - setupBarViews() + + backgroundLayer.frame = self.bounds + progressLayer.frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.width * CGFloat(self.progress), height: self.bounds.height)) + computeWaveForm() } - func setSamples(_ samples: [Float]) { - self.samples = samples - updateBarViews() - } - // MARK: - Private - private func setupBarViews() { - for layer in barViews { - layer.removeFromSuperlayer() - } + private func computeWaveForm() { + renderingQueue.async { + let path = UIBezierPath() - var barViews: [CALayer] = [] + let drawMappingFactor = self.bounds.size.height + let minimumGraphAmplitude: CGFloat = 1 - var xOffset: CGFloat = lineWidth / 2 + var xOffset: CGFloat = self.lineWidth / 2 + var index = 0 + + while xOffset < self.bounds.width - self.lineWidth { + let sample = CGFloat(index >= self.samples.count ? 1 : self.samples[index]) + let invertedDbSample = 1 - sample // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) + let drawingAmplitude = max(minimumGraphAmplitude, invertedDbSample * drawMappingFactor) - while xOffset < bounds.width - lineWidth { - let layer = CALayer() - layer.backgroundColor = primarylineColor.cgColor - layer.cornerRadius = lineWidth / 2 - layer.masksToBounds = true - layer.anchorPoint = CGPoint(x: 0, y: 0.5) - layer.frame = CGRect(x: xOffset, y: bounds.midY - lineWidth / 2, width: lineWidth, height: lineWidth) + path.move(to: CGPoint(x: xOffset, y: self.bounds.midY - drawingAmplitude / 2)) + path.addLine(to: CGPoint(x: xOffset, y: self.bounds.midY + drawingAmplitude / 2)) - self.layer.addSublayer(layer) + xOffset += self.lineWidth + self.linePadding - barViews.append(layer) + index += 1 + } - xOffset += lineWidth + linePadding + DispatchQueue.main.async { + self.backgroundLayer.path = path.cgPath + self.progressLayer.path = path.cgPath + } } - - self.barViews = barViews - - updateBarViews() } - - private func updateBarViews() { - let drawMappingFactor = bounds.size.height - let minimumGraphAmplitude: CGFloat = lineWidth - - let progressPosition = Int(floor(progress * Double(barViews.count))) - - for (index, layer) in barViews.enumerated() { - let sample = CGFloat(index >= samples.count ? 1 : samples[index]) - - let invertedDbSample = 1 - sample // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) - let drawingAmplitude = max(minimumGraphAmplitude, invertedDbSample * drawMappingFactor) - - layer.frame.origin.y = bounds.midY - drawingAmplitude / 2 - layer.frame.size.height = drawingAmplitude - - layer.backgroundColor = (index < progressPosition ? secondaryLineColor.cgColor : primarylineColor.cgColor) - } + + private func setupAndAdd(_ shapeLayer: CAShapeLayer, with color: UIColor) { +// shapeLayer.shouldRasterize = true + shapeLayer.drawsAsynchronously = true + shapeLayer.frame = self.bounds + shapeLayer.strokeColor = color.cgColor + shapeLayer.fillColor = color.cgColor + shapeLayer.lineCap = .round + shapeLayer.lineWidth = lineWidth + self.layer.addSublayer(shapeLayer) } } From a15c274002e4443c802fca62cb0f495476224615 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 5 Jul 2021 15:27:52 +0100 Subject: [PATCH 033/125] Show encrypted message notification content by default. --- Riot/Modules/Application/LegacyAppDelegate.m | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index ff81e5f571..be5ad0b46d 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -4201,18 +4201,16 @@ - (void)setupUserDefaults [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; + // Migrates old UserDefaults values if showDecryptedContentInNotifications hasn't been set if (!RiotSettings.shared.isUserDefaultsMigrated) { [RiotSettings.shared migrate]; } - // Now use RiotSettings and NSUserDefaults to store `showDecryptedContentInNotifications` setting option - // Migrate this information from main MXKAccount to RiotSettings, if value is not in UserDefaults - + // Show encrypted message notification content by default. if (!RiotSettings.shared.isShowDecryptedContentInNotificationsHasBeenSetOnce) { - MXKAccount *currentAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; - RiotSettings.shared.showDecryptedContentInNotifications = currentAccount.showDecryptedContentInNotifications; + RiotSettings.shared.showDecryptedContentInNotifications = YES; } } From b5f9e37ad10456a82790814ded7ec8314943b27e Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 5 Jul 2021 15:41:14 +0100 Subject: [PATCH 034/125] Update CHANGES.rst. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2e72d9c8e9..575f227685 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changes to be released in next version * 🙌 Improvements - * + * Notifications: Show decrypted content is enabled by default (#4519). 🐛 Bugfix * From d67461f01261525a105ce093cbd6b38355f07712 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Jul 2021 08:57:51 +0200 Subject: [PATCH 035/125] MXKeyBackup: trustForKeyBackupVersionFromCryptoQueue must consider MSK trust - code tweaks and optimizations --- .../VoiceMessageAttachmentCacheManager.swift | 20 +++--- .../VoiceMessageController.swift | 61 ++++++++---------- .../VoiceMessageMediaServiceProvider.swift | 49 +++----------- .../VoiceMessagePlaybackController.swift | 64 +++++++++++++------ .../VoiceMessageWaveformView.swift | 5 +- 5 files changed, 92 insertions(+), 107 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 03f27227f8..30704b6fa4 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -31,9 +31,9 @@ enum VoiceMessageAttachmentCacheManagerError: Error { Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array. */ private class CompletionWrapper { - let completion: (Result<(URL, TimeInterval, [Float]), Error>) -> Void + let completion: (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void - init(_ completion: @escaping (Result<(URL, TimeInterval, [Float]), Error>) -> Void) { + init(_ completion: @escaping (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void) { self.completion = completion } } @@ -42,18 +42,18 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() - private let workQueue: DispatchQueue +// private let workQueue: DispatchQueue private var completionCallbacks = [String: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var durations = [String: TimeInterval]() private var finalURLs = [String: URL]() - private init() { - workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) - } +// private init() { +// workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) +// } - func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, TimeInterval, [Float]), Error>) -> Void) { + func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) return @@ -70,7 +70,7 @@ class VoiceMessageAttachmentCacheManager { } if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] { - completion(Result.success((finalURL, duration, samples))) + completion(Result.success((identifier, finalURL, duration, samples))) return } @@ -79,7 +79,7 @@ class VoiceMessageAttachmentCacheManager { // } } - private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, Double, [Float]), Error>) -> Void) { + private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(String, URL, Double, [Float]), Error>) -> Void) { if var callbacks = completionCallbacks[identifier] { callbacks.append(CompletionWrapper(completion)) @@ -177,7 +177,7 @@ class VoiceMessageAttachmentCacheManager { let copy = callbacks.map { $0 } DispatchQueue.main.async { for wrapper in copy { - wrapper.completion(Result.success((url, duration, samples))) + wrapper.completion(Result.success((identifier, url, duration, samples))) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 662800be8e..233f9e7e75 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -44,9 +44,6 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private let _voiceMessageToolbarView: VoiceMessageToolbarView private var displayLink: CADisplayLink! - private var audioRecorder: VoiceMessageAudioRecorder? - - private var audioPlayer: VoiceMessageAudioPlayer? private var waveformAnalyser: WaveformAnalyzer? private var audioSamples: [Float] = [] @@ -89,9 +86,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - audioRecorder = mediaServiceProvider.audioRecorder() - audioRecorder?.registerDelegate(self) - audioRecorder?.recordWithOuputURL(temporaryFileURL) + mediaServiceProvider.audioRecorder.registerDelegate(self) + mediaServiceProvider.audioRecorder.recordWithOuputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -100,8 +96,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { isInLockedMode = false - audioRecorder?.stopRecording() - deleteRecordingAtURL(audioRecorder?.url) + mediaServiceProvider.audioRecorder.stopRecording() + deleteRecordingAtURL(mediaServiceProvider.audioRecorder.url) UINotificationFeedbackGenerator().notificationOccurred(.error) updateUI() } @@ -112,21 +108,21 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { - if audioPlayer?.isPlaying ?? false { - audioPlayer?.pause() + if mediaServiceProvider.audioPlayer.isPlaying { + mediaServiceProvider.audioPlayer.pause() } else { - audioPlayer?.play() + mediaServiceProvider.audioPlayer.play() } } func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) { - guard let url = audioRecorder?.url else { + guard let url = mediaServiceProvider.audioRecorder.url else { MXLog.error("Invalid audio recording URL") return } - audioPlayer?.stop() - audioRecorder?.stopRecording() + mediaServiceProvider.audioPlayer.stop() + mediaServiceProvider.audioRecorder.stopRecording() sendRecordingAtURL(url) @@ -179,24 +175,23 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private private func finishRecording() { - let recordDuration = audioRecorder?.currentTime - audioRecorder?.stopRecording() + let recordDuration = mediaServiceProvider.audioRecorder.currentTime + mediaServiceProvider.audioRecorder.stopRecording() - guard let url = audioRecorder?.url else { + guard let url = mediaServiceProvider.audioRecorder.url else { MXLog.error("Invalid audio recording URL") return } guard isInLockedMode else { - if recordDuration ?? 0 >= Constants.minimumRecordingDuration { + if recordDuration >= Constants.minimumRecordingDuration { sendRecordingAtURL(url) } return } - audioPlayer = mediaServiceProvider.audioPlayer() - audioPlayer?.registerDelegate(self) - audioPlayer?.loadContentFromURL(url) + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.loadContentFromURL(url) audioSamples = [] updateUI() @@ -244,7 +239,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private func updateUI() { - let shouldUpdateFromAudioPlayer = isInLockedMode && !(audioRecorder?.isRecording ?? false) + let shouldUpdateFromAudioPlayer = isInLockedMode && !mediaServiceProvider.audioRecorder.isRecording if shouldUpdateFromAudioPlayer { updateUIFromAudioPlayer() @@ -254,7 +249,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioRecorder() { - let isRecording = audioRecorder?.isRecording ?? false + let isRecording = mediaServiceProvider.audioRecorder.isRecording displayLink.isPaused = !isRecording @@ -264,11 +259,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, padSamplesArrayToSize(requiredNumberOfSamples) } - let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0 + let sample = mediaServiceProvider.audioRecorder.averagePowerForChannelNumber(0) audioSamples.insert(sample, at: 0) audioSamples.removeLast() - let currentTime = audioRecorder?.currentTime ?? 0.0 + let currentTime = mediaServiceProvider.audioRecorder.currentTime if currentTime >= Constants.maximumAudioRecordingDuration { finishRecording() @@ -292,16 +287,12 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioPlayer() { - guard let audioPlayer = audioPlayer else { - return - } - - guard let url = audioPlayer.url else { + guard let url = mediaServiceProvider.audioPlayer.url else { MXLog.error("Invalid audio player url.") return } - displayLink.isPaused = !audioPlayer.isPlaying + displayLink.isPaused = !mediaServiceProvider.audioPlayer.isPlaying let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() if audioSamples.count != requiredNumberOfSamples && requiredNumberOfSamples > 0 { @@ -322,11 +313,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } var details = VoiceMessageToolbarViewDetails() - details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) + details.state = (mediaServiceProvider.audioRecorder.isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (mediaServiceProvider.audioPlayer.isPlaying ? mediaServiceProvider.audioPlayer.currentTime : mediaServiceProvider.audioPlayer.duration))) details.audioSamples = audioSamples - details.isPlaying = audioPlayer.isPlaying - details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + details.isPlaying = mediaServiceProvider.audioPlayer.isPlaying + details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) _voiceMessageToolbarView.configureWithDetails(details) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 3c192a6793..7d455c0a3d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -18,63 +18,32 @@ import Foundation @objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate { - private let audioPlayers: NSHashTable - private let audioRecorders: NSHashTable - + let audioPlayer = VoiceMessageAudioPlayer() + var mediaIdentifier: String? + let audioRecorder = VoiceMessageAudioRecorder() + @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() private override init() { - audioPlayers = NSHashTable(options: .weakMemory) - audioRecorders = NSHashTable(options: .weakMemory) - } - - @objc func audioPlayer() -> VoiceMessageAudioPlayer { - let audioPlayer = VoiceMessageAudioPlayer() + super.init() audioPlayer.registerDelegate(self) - audioPlayers.add(audioPlayer) - return audioPlayer - } - - @objc func audioRecorder() -> VoiceMessageAudioRecorder { - let audioRecorder = VoiceMessageAudioRecorder() audioRecorder.registerDelegate(self) - audioRecorders.add(audioRecorder) - return audioRecorder } @objc func stopAllServices() { - stopAllServicesExcept(nil) + audioPlayer.stop() + audioRecorder.stopRecording() } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { - stopAllServicesExcept(audioPlayer) + audioRecorder.stopRecording() } // MARK: - VoiceMessageAudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - stopAllServicesExcept(audioRecorder) - } - - // MARK: - Private - - private func stopAllServicesExcept(_ service: AnyObject?) { - for audioPlayer in audioPlayers.allObjects { - if audioPlayer === service { - continue - } - - audioPlayer.stop() - } - - for audioRecoder in audioRecorders.allObjects { - if audioRecoder === service { - continue - } - - audioRecoder.stopRecording() - } + audioPlayer.stop() } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index f3a4af6032..f204f3e5c8 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -37,8 +37,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess }() private let cacheManager: VoiceMessageAttachmentCacheManager - - private let audioPlayer: VoiceMessageAudioPlayer + private let mediaServiceProvider: VoiceMessageMediaServiceProvider private var displayLink: CADisplayLink! private var samples: [Float] = [] private var duration: TimeInterval = 0 @@ -47,6 +46,9 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess private var state: VoiceMessagePlaybackControllerState = .stopped { didSet { + if state == .stopped || state == .error { + mediaServiceProvider.audioPlayer.deregisterDelegate(self) + } updateUI() displayLink.isPaused = (state != .playing) } @@ -57,11 +59,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess init(mediaServiceProvider: VoiceMessageMediaServiceProvider, cacheManager: VoiceMessageAttachmentCacheManager) { self.cacheManager = cacheManager + self.mediaServiceProvider = mediaServiceProvider playbackView = VoiceMessagePlaybackView.loadFromNib() - audioPlayer = mediaServiceProvider.audioPlayer() - audioPlayer.registerDelegate(self) playbackView.delegate = self displayLink = CADisplayLink(target: WeakDisplayLinkTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakDisplayLinkTarget.triggerSelector) @@ -82,20 +83,29 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess // MARK: - VoiceMessagePlaybackViewDelegate func voiceMessagePlaybackViewDidRequestPlaybackToggle() { - if audioPlayer.isPlaying { - audioPlayer.pause() + if mediaServiceProvider.mediaIdentifier == attachment?.eventId { + if mediaServiceProvider.audioPlayer.isPlaying { + mediaServiceProvider.audioPlayer.pause() + } else { + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.play() + } } else { - if let urlToLoad = urlToLoad { - audioPlayer.loadContentFromURL(urlToLoad) + if let url = urlToLoad { + mediaServiceProvider.mediaIdentifier = attachment?.eventId + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.loadContentFromURL(url) + mediaServiceProvider.audioPlayer.play() } - urlToLoad = nil - audioPlayer.play() } } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { + if audioPlayer.url != self.urlToLoad { + state = .stopped + } updateUI() } @@ -139,8 +149,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) - details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: mediaServiceProvider.audioPlayer.currentTime)) + details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) } details.loading = self.loading @@ -152,23 +162,37 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return } + mediaServiceProvider.audioPlayer.deregisterDelegate(self) + self.state = .stopped self.loading = true + self.samples = [] updateUI() - // TODO: manage a unique instance of audio player. - if audioPlayer.isPlaying || audioPlayer.currentTime > 0 { - audioPlayer.stop() - } - let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() cacheManager.loadAttachment(attachment, numberOfSamples: requiredNumberOfSamples) { result in switch result { case .success(let result): + guard result.0 == attachment.eventId else { + return + } + self.loading = false - self.urlToLoad = result.0 - self.duration = result.1 - self.samples = result.2 + self.urlToLoad = result.1 + self.duration = result.2 + self.samples = result.3 + + if self.mediaServiceProvider.mediaIdentifier == self.attachment?.eventId { + self.mediaServiceProvider.audioPlayer.registerDelegate(self) + if self.mediaServiceProvider.audioPlayer.isPlaying { + self.state = .playing + } else if self.mediaServiceProvider.audioPlayer.currentTime > 0 { + self.state = .paused + } else { + self.state = .stopped + } + } + self.updateUI() case .failure: self.state = .error diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift index d180236300..c0abee8439 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -46,7 +46,10 @@ class VoiceMessageWaveformView: UIView { var progress = 0.0 { didSet { + CATransaction.begin() + CATransaction.setDisableActions(true) progressLayer.frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.width * CGFloat(self.progress), height: self.bounds.height)) + CATransaction.commit() } } @@ -109,8 +112,6 @@ class VoiceMessageWaveformView: UIView { } private func setupAndAdd(_ shapeLayer: CAShapeLayer, with color: UIColor) { -// shapeLayer.shouldRasterize = true - shapeLayer.drawsAsynchronously = true shapeLayer.frame = self.bounds shapeLayer.strokeColor = color.cgColor shapeLayer.fillColor = color.cgColor From 9f6f27cbe0d6ab86b84d3379e4b596608eff9d60 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Jul 2021 09:22:22 +0200 Subject: [PATCH 036/125] Updated CHANGES.rst --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 679b2f8f09..5e10fa45c9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Changes to be released in next version 🙌 Improvements * DesignKit: Add Fonts (#4356). + * Room: Added support for Voice Messages (#4090, #4091, #4092, #4094, #4095, #4096) 🐛 Bugfix * SSO: Handle login callback URL with HTML entities (#4129). From 072509e930c3ba368f3a0e8a4e73b87f8ef4396c Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Jul 2021 15:17:22 +0200 Subject: [PATCH 037/125] Update Riot/Modules/Room/RoomViewController.m Co-authored-by: ismailgulek --- Riot/Modules/Room/RoomViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index c4fab0c29e..3338f979be 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -240,7 +240,7 @@ @interface RoomViewController () Date: Tue, 6 Jul 2021 15:17:54 +0200 Subject: [PATCH 038/125] Update Riot/Assets/en.lproj/Vector.strings Co-authored-by: ismailgulek --- Riot/Assets/en.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index b15502a615..eef3e9aa36 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1673,4 +1673,4 @@ Tap the + to start adding people."; "voice_message_release_to_send" = "Release to send"; "voice_message_remaining_recording_time" = "%@s left"; -"voice_message_stop_locked_mode_recording" = "Tap on the wavelenghth to stop and playback"; +"voice_message_stop_locked_mode_recording" = "Tap on the wavelength to stop and playback"; From 1c90b1cee084ab3951668fc8d400e955b8e778b9 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 6 Jul 2021 16:14:32 +0100 Subject: [PATCH 039/125] Begin removing contacts section from PeopleViewController. --- Riot/Modules/People/PeopleViewController.m | 253 ++----------------- Riot/Modules/TabBar/MasterTabBarController.h | 1 + 2 files changed, 21 insertions(+), 233 deletions(-) diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 5985ce3468..4ec4b2d92a 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -14,7 +14,6 @@ limitations under the License. */ -#import #import "PeopleViewController.h" #import "UIViewController+RiotSearch.h" @@ -25,17 +24,11 @@ #import "RecentTableViewCell.h" #import "InviteRecentTableViewCell.h" -#import "ContactTableViewCell.h" - #import "Riot-Swift.h" @interface PeopleViewController () { NSInteger directRoomsSectionNumber; - - ContactsDataSource *contactsDataSource; - NSInteger contactsSectionNumber; - RecentsDataSource *recentsDataSource; } @@ -55,7 +48,6 @@ - (void)finalizeInit [super finalizeInit]; directRoomsSectionNumber = 0; - contactsSectionNumber = 0; self.screenName = @"People"; } @@ -92,35 +84,9 @@ - (void)didReceiveMemoryWarning // Dispose of any resources that can be recreated. } -- (void)destroy -{ - contactsDataSource.delegate = nil; - [contactsDataSource destroy]; - contactsDataSource = nil; - - [super destroy]; -} - - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - if (BuildSettings.allowLocalContactsAccess) - { - // Check whether the access to the local contacts has not been already asked - // and check that the user has decided to use or not to use an identity server - if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined - || !contactsDataSource.mxSession.hasAccountDataIdentityServerValue) - { - // Allow by default the local contacts sync in order to discover matrix users. - // This setting change will trigger the loading of the local contacts, which will automatically - // ask user permission to access their local contacts. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - - // Refresh the local contacts list. - [[MXKContactManager sharedManager] refreshLocalContacts]; - } [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_people", @"Vector", nil); [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor; @@ -147,17 +113,6 @@ - (void)displayList:(MXKRecentsDataSource *)listDataSource { recentsDataSource = (RecentsDataSource*)listDataSource; } - - if (BuildSettings.allowLocalContactsAccess) - { - if (!contactsDataSource) - { - // Prepare its contacts data source - contactsDataSource = [[ContactsDataSource alloc] initWithMatrixSession:listDataSource.mxSession]; - contactsDataSource.contactCellAccessoryImage = [[UIImage imageNamed: @"disclosure_icon"] vc_tintedImageUsingColor:ThemeService.shared.theme.textSecondaryColor]; - contactsDataSource.delegate = self; - } - } } #pragma mark - MXKDataSourceDelegate @@ -185,28 +140,18 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView directRoomsSectionNumber = [self.dataSource numberOfSectionsInTableView:self.recentsTableView]; } - // Retrieve the current number of sections related to the contacts - contactsSectionNumber = [contactsDataSource numberOfSectionsInTableView:self.recentsTableView]; - - return (directRoomsSectionNumber + contactsSectionNumber); + return directRoomsSectionNumber; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger count = 0; + // FIXME: Should this still need to check the section? Where do invites come in? if (section < directRoomsSectionNumber) { count = [self.dataSource tableView:tableView numberOfRowsInSection:section]; } - else - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - count = [contactsDataSource tableView:tableView numberOfRowsInSection:section]; - } - } return count; } @@ -215,18 +160,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N { NSInteger section = indexPath.section; + // FIXME: Should this still need to check the section? Where do invites come in? if (section < directRoomsSectionNumber) { return [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath]; } - else - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource tableView:tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; - } - } // Return a fake cell to prevent app from crashing. return [[UITableViewCell alloc] init]; @@ -236,18 +174,11 @@ - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *) { NSInteger section = indexPath.section; + // FIXME: Should this still need to check the section? Where do invites come in? if (section < directRoomsSectionNumber) { return [self.dataSource tableView:tableView canEditRowAtIndexPath:indexPath]; } - else - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource tableView:tableView canEditRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; - } - } return NO; } @@ -256,18 +187,10 @@ - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *) - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + // FIXME: Should this still need to check the section? Where do invites come in? if (section >= directRoomsSectionNumber) { - // Let the contact dataSource provide the height of the section header. - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource heightForHeaderInSection:section]; - } - else - { - return 0.0; - } + return 0.0; } return [super tableView:tableView heightForHeaderInSection:section]; @@ -275,37 +198,10 @@ - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSIntege - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + // FIXME: Should this still need to check the section? Where do invites come in? if (section >= directRoomsSectionNumber) { - // Let the contact dataSource provide the section header. - CGRect frame = [tableView rectForHeaderInSection:section]; - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - UIView *sectionHeader = [contactsDataSource viewForHeaderInSection:section withFrame:frame]; - sectionHeader.tag = section + directRoomsSectionNumber; - - if (self.enableStickyHeaders) - { - while (sectionHeader.gestureRecognizers.count) - { - UIGestureRecognizer *gestureRecognizer = sectionHeader.gestureRecognizers.lastObject; - [sectionHeader removeGestureRecognizer:gestureRecognizer]; - } - - // Handle tap gesture - UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnSectionHeader:)]; - [tap setNumberOfTouchesRequired:1]; - [tap setNumberOfTapsRequired:1]; - [sectionHeader addGestureRecognizer:tap]; - } - - return sectionHeader; - } - else - { - return nil; - } + return nil; } return [super tableView:tableView viewForHeaderInSection:section]; @@ -314,23 +210,11 @@ - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger section = indexPath.section; + + // FIXME: Should this still need to check the section? Where do invites come in? if (section >= directRoomsSectionNumber) { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - if ([contactsDataSource contactAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]) - { - // Return the default height of the contact cell - return 74.0; - } - - return 50; - } - else - { - return 0.0; - } + return 0.0; } return [super tableView:tableView heightForRowAtIndexPath:indexPath]; @@ -339,26 +223,11 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger section = indexPath.section; + // FIXME: Should this still need to check the section? Where do invites come in? if (section >= directRoomsSectionNumber) { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - MXKContact *mxkContact = [contactsDataSource contactAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; - - if (mxkContact) - { - [[AppDelegate theDelegate].masterTabBarController selectContact:mxkContact]; - - // Keep selected the cell by default. - return; - } - } - else - { - [tableView deselectRowAtIndexPath:indexPath animated:NO]; - return; - } + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + return; } return [super tableView:tableView didSelectRowAtIndexPath:indexPath]; @@ -371,16 +240,13 @@ - (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSI CGRect frame = [tableView rectForHeaderInSection:section]; frame.size.height = self.stickyHeaderHeight; + // FIXME: Should this still need to check the section? Where do invites come in? if (section >= directRoomsSectionNumber) { - // Let the contact dataSource provide this header. - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource viewForStickyHeaderInSection:section withFrame:frame]; - } + return nil; } - else if (recentsDataSource) + + if (recentsDataSource) { return [recentsDataSource viewForStickyHeaderInSection:section withFrame:frame]; } @@ -396,41 +262,7 @@ - (void)refreshCurrentSelectedCell:(BOOL)forceVisible return; } - // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. - NSIndexPath *currentSelectedCellIndexPath = nil; - MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; - if (masterTabBarController.currentContactDetailViewController) - { - // Look for the rank of this selected contact - currentSelectedCellIndexPath = [contactsDataSource cellIndexPathWithContact:masterTabBarController.selectedContact]; - - if (currentSelectedCellIndexPath) - { - // Select the right row - currentSelectedCellIndexPath = [NSIndexPath indexPathForRow:currentSelectedCellIndexPath.row inSection:(directRoomsSectionNumber + currentSelectedCellIndexPath.section)]; - [self.recentsTableView selectRowAtIndexPath:currentSelectedCellIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; - - if (forceVisible) - { - // Scroll table view to make the selected row appear at second position - NSInteger topCellIndexPathRow = currentSelectedCellIndexPath.row ? currentSelectedCellIndexPath.row - 1: currentSelectedCellIndexPath.row; - NSIndexPath* indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:currentSelectedCellIndexPath.section]; - [self.recentsTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; - } - } - else - { - NSIndexPath *indexPath = [self.recentsTableView indexPathForSelectedRow]; - if (indexPath) - { - [self.recentsTableView deselectRowAtIndexPath:indexPath animated:NO]; - } - } - } - else - { - [super refreshCurrentSelectedCell:forceVisible]; - } + [super refreshCurrentSelectedCell:forceVisible]; } - (void)onPlusButtonPressed @@ -449,24 +281,6 @@ - (void)scrollToNextRoomWithMissedNotifications } } -#pragma mark - UISearchBarDelegate - -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - // Apply filter on contact source - [contactsDataSource searchWithPattern:searchText forceReset:NO]; - - [super searchBar:searchBar textDidChange:searchText]; -} - -- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar -{ - // Reset filtering - [contactsDataSource searchWithPattern:nil forceReset:NO]; - - [super searchBarCancelButtonClicked:searchBar]; -} - #pragma mark - Empty view management - (void)updateEmptyView @@ -504,34 +318,7 @@ - (NSUInteger)totalItemCounts { return recentsDataSource.invitesCellDataArray.count + recentsDataSource.conversationCellDataArray.count - + recentsDataSource.peopleCellDataArray.count - + [self numberOfContactsInContactsDataSource]; -} - -- (NSUInteger)numberOfContactsInContactsDataSource -{ - BOOL areLocalContactsAccessAuthorized = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized; - - NSInteger nbOfItemsInContactDataSource = 0; - - for (NSInteger i = 0; i < contactsSectionNumber; i++) - { - nbOfItemsInContactDataSource += [contactsDataSource tableView:self.recentsTableView numberOfRowsInSection:i]; - } - - NSInteger numberOfContactsInContactsDataSource; - - // No local contacts to show and no search in directory - if (!areLocalContactsAccessAuthorized && contactsSectionNumber == 1 && nbOfItemsInContactDataSource <= 1) - { - numberOfContactsInContactsDataSource = 0; - } - else - { - numberOfContactsInContactsDataSource = nbOfItemsInContactDataSource; - } - - return numberOfContactsInContactsDataSource; + + recentsDataSource.peopleCellDataArray.count; } @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index 6ef6619489..b558fc1106 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -172,6 +172,7 @@ @property (nonatomic, readonly) MXKRoomDataSource *selectedRoomDataSource; @property (nonatomic, readonly) RoomPreviewData *selectedRoomPreviewData; +// TODO: Check if this is needed anymore as the New Chat dialog is model // References on the currently selected contact and its view controller @property (nonatomic, readonly) ContactDetailsViewController *currentContactDetailViewController; @property (nonatomic, readonly) MXKContact *selectedContact; From 450cc61fe6f59169ffd4f587e4191189afc1977f Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 6 Jul 2021 16:37:11 +0100 Subject: [PATCH 040/125] Remove check. --- Riot/Modules/TabBar/MasterTabBarController.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index b558fc1106..6ef6619489 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -172,7 +172,6 @@ @property (nonatomic, readonly) MXKRoomDataSource *selectedRoomDataSource; @property (nonatomic, readonly) RoomPreviewData *selectedRoomPreviewData; -// TODO: Check if this is needed anymore as the New Chat dialog is model // References on the currently selected contact and its view controller @property (nonatomic, readonly) ContactDetailsViewController *currentContactDetailViewController; @property (nonatomic, readonly) MXKContact *selectedContact; From dd4c85fad76841683d9a16e04f3d8fd78601935e Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 6 Jul 2021 17:43:16 +0100 Subject: [PATCH 041/125] Tidy up PeopleViewController data source methods. --- Riot/Modules/People/PeopleViewController.m | 62 +++++++++------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 4ec4b2d92a..79cbf86e7e 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -145,49 +145,43 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - NSInteger count = 0; - - // FIXME: Should this still need to check the section? Where do invites come in? - if (section < directRoomsSectionNumber) + // FIXME: Should this need to check the section? + if (section >= directRoomsSectionNumber) { - count = [self.dataSource tableView:tableView numberOfRowsInSection:section]; + return 0; } - return count; + return [self.dataSource tableView:tableView numberOfRowsInSection:section]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - NSInteger section = indexPath.section; - - // FIXME: Should this still need to check the section? Where do invites come in? - if (section < directRoomsSectionNumber) + // FIXME: Should this need to check the section? + if (indexPath.section >= directRoomsSectionNumber) { - return [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath]; + // Return a fake cell to prevent app from crashing. + return [[UITableViewCell alloc] init]; } - // Return a fake cell to prevent app from crashing. - return [[UITableViewCell alloc] init]; + return [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath]; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { - NSInteger section = indexPath.section; - - // FIXME: Should this still need to check the section? Where do invites come in? - if (section < directRoomsSectionNumber) + // FIXME: Should this need to check the section? + if (indexPath.section >= directRoomsSectionNumber) { - return [self.dataSource tableView:tableView canEditRowAtIndexPath:indexPath]; + return NO; } - return NO; + return [self.dataSource tableView:tableView canEditRowAtIndexPath:indexPath]; } #pragma mark - UITableView delegate - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - // FIXME: Should this still need to check the section? Where do invites come in? + // FIXME: Should this need to check the section? if (section >= directRoomsSectionNumber) { return 0.0; @@ -198,7 +192,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSIntege - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { - // FIXME: Should this still need to check the section? Where do invites come in? + // FIXME: Should this need to check the section? if (section >= directRoomsSectionNumber) { return nil; @@ -209,10 +203,8 @@ - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - NSInteger section = indexPath.section; - - // FIXME: Should this still need to check the section? Where do invites come in? - if (section >= directRoomsSectionNumber) + // FIXME: Should this need to check the section? + if (indexPath.section >= directRoomsSectionNumber) { return 0.0; } @@ -222,9 +214,8 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - NSInteger section = indexPath.section; - // FIXME: Should this still need to check the section? Where do invites come in? - if (section >= directRoomsSectionNumber) + // FIXME: Should this need to check the section? + if (indexPath.section >= directRoomsSectionNumber) { [tableView deselectRowAtIndexPath:indexPath animated:NO]; return; @@ -237,21 +228,16 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath - (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section { - CGRect frame = [tableView rectForHeaderInSection:section]; - frame.size.height = self.stickyHeaderHeight; - - // FIXME: Should this still need to check the section? Where do invites come in? - if (section >= directRoomsSectionNumber) + // FIXME: Should this need to check the section? + if (section >= directRoomsSectionNumber || recentsDataSource == nil) { return nil; } - if (recentsDataSource) - { - return [recentsDataSource viewForStickyHeaderInSection:section withFrame:frame]; - } + CGRect frame = [tableView rectForHeaderInSection:section]; + frame.size.height = self.stickyHeaderHeight; - return nil; + return [recentsDataSource viewForStickyHeaderInSection:section withFrame:frame]; } - (void)refreshCurrentSelectedCell:(BOOL)forceVisible From e492d2b0771b6e528573bd9da80cd159e2e5e152 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Jul 2021 23:03:56 +0200 Subject: [PATCH 042/125] #4090 - Update after review --- Riot/Assets/third_party_licenses.html | 198 +++++++++++++++++- .../MXKRoomBubbleTableViewCell+Riot.m | 2 +- Riot/Generated/Strings.swift | 2 +- .../VoiceMessage/VoiceMessageBubbleCell.swift | 6 +- .../VoiceMessageAttachmentCacheManager.swift | 11 +- .../VoiceMessageAudioConverter.swift | 36 +++- .../VoiceMessageController.swift | 2 +- .../VoiceMessagePlaybackController.swift | 2 +- .../VoiceMessagePlaybackView.swift | 1 + ...splayLinkTarget.swift => WeakTarget.swift} | 4 +- 10 files changed, 240 insertions(+), 24 deletions(-) rename Riot/Utils/{WeakDisplayLinkTarget.swift => WeakTarget.swift} (90%) diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index 4dfc32ac72..6e491fdcae 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1688,7 +1688,203 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

- + +

  • + DSWaveformImage (https://github.com/dmrschmidt/DSWaveformImage) +

    + The MIT License (MIT) +

    + Copyright (c) 2013 Dennis Schmidt +

    + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +

    +
  • +
  • + ffmpeg-kit-ios-audio (https://github.com/tanersener/ffmpeg-kit) +

    +
    +                   GNU LESSER GENERAL PUBLIC LICENSE
    +                       Version 3, 29 June 2007
    +
    + Copyright (C) 2007 Free Software Foundation, Inc. 
    + Everyone is permitted to copy and distribute verbatim copies
    + of this license document, but changing it is not allowed.
    +
    +
    +  This version of the GNU Lesser General Public License incorporates
    +the terms and conditions of version 3 of the GNU General Public
    +License, supplemented by the additional permissions listed below.
    +
    +  0. Additional Definitions.
    +
    +  As used herein, "this License" refers to version 3 of the GNU Lesser
    +General Public License, and the "GNU GPL" refers to version 3 of the GNU
    +General Public License.
    +
    +  "The Library" refers to a covered work governed by this License,
    +other than an Application or a Combined Work as defined below.
    +
    +  An "Application" is any work that makes use of an interface provided
    +by the Library, but which is not otherwise based on the Library.
    +Defining a subclass of a class defined by the Library is deemed a mode
    +of using an interface provided by the Library.
    +
    +  A "Combined Work" is a work produced by combining or linking an
    +Application with the Library.  The particular version of the Library
    +with which the Combined Work was made is also called the "Linked
    +Version".
    +
    +  The "Minimal Corresponding Source" for a Combined Work means the
    +Corresponding Source for the Combined Work, excluding any source code
    +for portions of the Combined Work that, considered in isolation, are
    +based on the Application, and not on the Linked Version.
    +
    +  The "Corresponding Application Code" for a Combined Work means the
    +object code and/or source code for the Application, including any data
    +and utility programs needed for reproducing the Combined Work from the
    +Application, but excluding the System Libraries of the Combined Work.
    +
    +  1. Exception to Section 3 of the GNU GPL.
    +
    +  You may convey a covered work under sections 3 and 4 of this License
    +without being bound by section 3 of the GNU GPL.
    +
    +  2. Conveying Modified Versions.
    +
    +  If you modify a copy of the Library, and, in your modifications, a
    +facility refers to a function or data to be supplied by an Application
    +that uses the facility (other than as an argument passed when the
    +facility is invoked), then you may convey a copy of the modified
    +version:
    +
    +   a) under this License, provided that you make a good faith effort to
    +   ensure that, in the event an Application does not supply the
    +   function or data, the facility still operates, and performs
    +   whatever part of its purpose remains meaningful, or
    +
    +   b) under the GNU GPL, with none of the additional permissions of
    +   this License applicable to that copy.
    +
    +  3. Object Code Incorporating Material from Library Header Files.
    +
    +  The object code form of an Application may incorporate material from
    +a header file that is part of the Library.  You may convey such object
    +code under terms of your choice, provided that, if the incorporated
    +material is not limited to numerical parameters, data structure
    +layouts and accessors, or small macros, inline functions and templates
    +(ten or fewer lines in length), you do both of the following:
    +
    +   a) Give prominent notice with each copy of the object code that the
    +   Library is used in it and that the Library and its use are
    +   covered by this License.
    +
    +   b) Accompany the object code with a copy of the GNU GPL and this license
    +   document.
    +
    +  4. Combined Works.
    +
    +  You may convey a Combined Work under terms of your choice that,
    +taken together, effectively do not restrict modification of the
    +portions of the Library contained in the Combined Work and reverse
    +engineering for debugging such modifications, if you also do each of
    +the following:
    +
    +   a) Give prominent notice with each copy of the Combined Work that
    +   the Library is used in it and that the Library and its use are
    +   covered by this License.
    +
    +   b) Accompany the Combined Work with a copy of the GNU GPL and this license
    +   document.
    +
    +   c) For a Combined Work that displays copyright notices during
    +   execution, include the copyright notice for the Library among
    +   these notices, as well as a reference directing the user to the
    +   copies of the GNU GPL and this license document.
    +
    +   d) Do one of the following:
    +
    +       0) Convey the Minimal Corresponding Source under the terms of this
    +       License, and the Corresponding Application Code in a form
    +       suitable for, and under terms that permit, the user to
    +       recombine or relink the Application with a modified version of
    +       the Linked Version to produce a modified Combined Work, in the
    +       manner specified by section 6 of the GNU GPL for conveying
    +       Corresponding Source.
    +
    +       1) Use a suitable shared library mechanism for linking with the
    +       Library.  A suitable mechanism is one that (a) uses at run time
    +       a copy of the Library already present on the user's computer
    +       system, and (b) will operate properly with a modified version
    +       of the Library that is interface-compatible with the Linked
    +       Version.
    +
    +   e) Provide Installation Information, but only if you would otherwise
    +   be required to provide such information under section 6 of the
    +   GNU GPL, and only to the extent that such information is
    +   necessary to install and execute a modified version of the
    +   Combined Work produced by recombining or relinking the
    +   Application with a modified version of the Linked Version. (If
    +   you use option 4d0, the Installation Information must accompany
    +   the Minimal Corresponding Source and Corresponding Application
    +   Code. If you use option 4d1, you must provide the Installation
    +   Information in the manner specified by section 6 of the GNU GPL
    +   for conveying Corresponding Source.)
    +
    +  5. Combined Libraries.
    +
    +  You may place library facilities that are a work based on the
    +Library side by side in a single library together with other library
    +facilities that are not Applications and are not covered by this
    +License, and convey such a combined library under terms of your
    +choice, if you do both of the following:
    +
    +   a) Accompany the combined library with a copy of the same work based
    +   on the Library, uncombined with any other library facilities,
    +   conveyed under the terms of this License.
    +
    +   b) Give prominent notice with the combined library that part of it
    +   is a work based on the Library, and explaining where to find the
    +   accompanying uncombined form of the same work.
    +
    +  6. Revised Versions of the GNU Lesser General Public License.
    +
    +  The Free Software Foundation may publish revised and/or new versions
    +of the GNU Lesser General Public License from time to time. Such new
    +versions will be similar in spirit to the present version, but may
    +differ in detail to address new problems or concerns.
    +
    +  Each version is given a distinguishing version number. If the
    +Library as you received it specifies that a certain numbered version
    +of the GNU Lesser General Public License "or any later version"
    +applies to it, you have the option of following the terms and
    +conditions either of that published version or of any later version
    +published by the Free Software Foundation. If the Library as you
    +received it does not specify a version number of the GNU Lesser
    +General Public License, you may choose any version of the GNU Lesser
    +General Public License ever published by the Free Software Foundation.
    +
    +  If the Library as you received it specifies that a proxy can decide
    +whether future versions of the GNU Lesser General Public License shall
    +apply, that proxy's public statement of acceptance of any version is
    +permanent authorization for you to choose that version for the
    +Library.
    +        
    +
  • diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index a85ea69be7..11a0fa5a11 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -397,7 +397,7 @@ - (void)setBlurred:(BOOL)blurred } // Move this view in front - [self.contentView bringSubviewToFront:self.bubbleOverlayContainer]; + [self.bubbleOverlayContainer.superview bringSubviewToFront:self.bubbleOverlayContainer]; } else { diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ddf58c6bb8..57f016874d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4846,7 +4846,7 @@ internal enum VectorL10n { internal static func voiceMessageRemainingRecordingTime(_ p1: String) -> String { return VectorL10n.tr("Vector", "voice_message_remaining_recording_time", p1) } - /// Tap on the wavelenghth to stop and playback + /// Tap on the wavelength to stop and playback internal static var voiceMessageStopLockedModeRecording: String { return VectorL10n.tr("Vector", "voice_message_stop_locked_mode_recording") } diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift index dbf36d15eb..397fc9fe76 100644 --- a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift @@ -31,12 +31,15 @@ class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplaya fatalError("Invalid attachment type passed to a voice message cell.") } - playbackController.attachment = data.attachment + if playbackController.attachment != data.attachment { + playbackController.attachment = data.attachment + } } override func setupViews() { super.setupViews() + bubbleCellContentView?.backgroundColor = .clear bubbleCellContentView?.showSenderInfo = true bubbleCellContentView?.showPaginationTitle = false @@ -46,7 +49,6 @@ class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplaya playbackController = VoiceMessagePlaybackController(mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider, cacheManager: VoiceMessageAttachmentCacheManager.sharedManager) - bubbleCellContentView?.addSubview(playbackController.playbackView) contentView.vc_addSubViewMatchingParent(playbackController.playbackView) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 30704b6fa4..d15a0c990d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -42,16 +42,13 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() -// private let workQueue: DispatchQueue - private var completionCallbacks = [String: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var durations = [String: TimeInterval]() private var finalURLs = [String: URL]() -// private init() { -// workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) -// } + private init() { + } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { @@ -74,9 +71,7 @@ class VoiceMessageAttachmentCacheManager { return } -// workQueue.async { - self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) -// } + self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(String, URL, Double, [Float]), Error>) -> Void) { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 6c8f5cadb2..3d4ef1df6c 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -34,13 +34,35 @@ struct VoiceMessageAudioConverter { } static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result) -> Void) { - DispatchQueue.global(qos: .userInteractive).async { - let mediaInfoSession = FFprobeKit.getMediaInformation(sourceURL.path) - let mediaInfo = mediaInfoSession?.getMediaInformation() - if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") { - completion(.success(duration)) - } else { - completion(.failure(.generic("Failed to get media duration"))) + FFprobeKit.getMediaInformationAsync(sourceURL.path) { session in + guard let session = session as? MediaInformationSession else { + completion(.failure(.generic("Invalid session"))) + return + } + + guard let returnCode = session.getReturnCode() else { + completion(.failure(.generic("Invalid return code"))) + return + } + + DispatchQueue.main.async { + if returnCode.isSuccess() { + let mediaInfo = session.getMediaInformation() + if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") { + completion(.success(duration)) + } else { + completion(.failure(.generic("Failed to get media duration"))) + } + } else if returnCode.isCancel() { + completion(.failure(.cancelled)) + } else { + completion(.failure(.generic(String(returnCode.getValue())))) + MXLog.error(""" + getMediaInformationAsync failed with state: \(String(describing: FFmpegKitConfig.sessionState(toString: session.getState()))), \ + returnCode: \(String(describing: returnCode)), \ + stackTrace: \(String(describing: session.getFailStackTrace())) + """) + } } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 233f9e7e75..a574b8bb5d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -65,7 +65,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, _voiceMessageToolbarView.delegate = self - displayLink = CADisplayLink(target: WeakDisplayLinkTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakDisplayLinkTarget.triggerSelector) + displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index f204f3e5c8..57aa4bba8a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -65,7 +65,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess playbackView.delegate = self - displayLink = CADisplayLink(target: WeakDisplayLinkTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakDisplayLinkTarget.triggerSelector) + displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) displayLink.isPaused = true displayLink.add(to: .current, forMode: .common) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index 4eeb012425..c749bcd47f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -102,6 +102,7 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { return } + self.backgroundColor = theme.colors.background playButton.backgroundColor = theme.colors.separator backgroundView.backgroundColor = theme.colors.quinaryContent _waveformView.primarylineColor = theme.colors.quarterlyContent diff --git a/Riot/Utils/WeakDisplayLinkTarget.swift b/Riot/Utils/WeakTarget.swift similarity index 90% rename from Riot/Utils/WeakDisplayLinkTarget.swift rename to Riot/Utils/WeakTarget.swift index 5cd2e2eb17..695df92e8a 100644 --- a/Riot/Utils/WeakDisplayLinkTarget.swift +++ b/Riot/Utils/WeakTarget.swift @@ -20,11 +20,11 @@ import Foundation Used to avoid retain cycles by creating a proxy that holds a weak reference to the original object. One example of that would be using CADisplayLink, which strongly retains its target, when manually invalidating it is unfeasable. */ -class WeakDisplayLinkTarget: NSObject { +class WeakTarget: NSObject { private(set) weak var target: AnyObject? let selector: Selector - static let triggerSelector = #selector(WeakDisplayLinkTarget.handleTick(parameter:)) + static let triggerSelector = #selector(WeakTarget.handleTick(parameter:)) init(_ target: AnyObject, selector: Selector) { self.target = target From b5c3cbe93e60ecf139dc38724d22f54b77eadf59 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 7 Jul 2021 11:48:23 +0100 Subject: [PATCH 043/125] Don't include directorySection in RecentsDataSource when in RecentsDataSourceModeRooms mode. --- Config/BuildSettings.swift | 2 +- .../Modules/Common/Recents/DataSources/RecentsDataSource.m | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 3cfcf65dc8..180c590334 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -148,7 +148,7 @@ final class BuildSettings: NSObject { // MARK: - Public rooms Directory - static let publicRoomsShowDirectory: Bool = true + #warning("Unused build setting: should this be implemented in ShowDirectory?") static let publicRoomsAllowServerChange: Bool = true // List of homeservers for the public rooms directory static let publicRoomsDirectoryServers = [ diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 3857f61fc4..a28236c04f 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -483,13 +483,6 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView conversationSection = sectionsCount++; } - if (_recentsDataSourceMode == RecentsDataSourceModeRooms - && BuildSettings.publicRoomsShowDirectory) - { - // Add the directory section after "ROOMS" - directorySection = sectionsCount++; - } - if (self.lowPriorityCellDataArray.count > 0) { lowPrioritySection = sectionsCount++; From 69e2012b2dd153dd0a70a4de055b0e914f5d3817 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 7 Jul 2021 12:58:11 +0100 Subject: [PATCH 044/125] Remove any logic from directory section from RoomsViewController. Includes removal of segue to DirectoryServerPickerViewController in Main.storyboard. --- Riot/Assets/Base.lproj/Main.storyboard | 40 +---- Riot/Modules/Rooms/RoomsViewController.m | 206 ----------------------- 2 files changed, 2 insertions(+), 244 deletions(-) diff --git a/Riot/Assets/Base.lproj/Main.storyboard b/Riot/Assets/Base.lproj/Main.storyboard index c562c4d0b5..0fc069dcce 100644 --- a/Riot/Assets/Base.lproj/Main.storyboard +++ b/Riot/Assets/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -356,22 +356,6 @@ - - - - - - - - - - - - - - - - @@ -479,7 +463,6 @@ - @@ -560,25 +543,6 @@ - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index a9dc73b5d4..b3c820b730 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -18,16 +18,11 @@ #import "RecentsDataSource.h" -#import "DirectoryServerPickerViewController.h" - #import "Riot-Swift.h" @interface RoomsViewController () { RecentsDataSource *recentsDataSource; - - // The animated view displayed at the table view bottom when paginating the room directory - UIView* footerSpinnerView; } @end @@ -74,24 +69,12 @@ - (void)viewWillAppear:(BOOL)animated [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_rooms", @"Vector", nil); [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor; - // TODO: Notify RiotSettings.shared.showNSFWPublicRooms change for iPad as viewWillAppear may not be called - recentsDataSource.publicRoomsDirectoryDataSource.showNSFWRooms = RiotSettings.shared.showNSFWPublicRooms; - if ([self.dataSource isKindOfClass:RecentsDataSource.class]) { - BOOL isFirstTime = (recentsDataSource != self.dataSource); - // Take the lead on the shared data source. recentsDataSource = (RecentsDataSource*)self.dataSource; recentsDataSource.areSectionsShrinkable = NO; [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeRooms]; - - if (isFirstTime) - { - // The first time the screen is displayed, make publicRoomsDirectoryDataSource - // start loading data - [recentsDataSource.publicRoomsDirectoryDataSource paginate:nil failure:nil]; - } } } @@ -121,19 +104,6 @@ - (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSI return [recentsDataSource viewForHeaderInSection:section withFrame:frame]; } -- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo -{ - if ([actionIdentifier isEqualToString:kRecentsDataSourceTapOnDirectoryServerChange]) - { - // Show the directory server picker - [self performSegueWithIdentifier:@"presentDirectoryServerPicker" sender:self]; - } - else - { - [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo]; - } -} - - (void)onPlusButtonPressed { [self showRoomDirectory]; @@ -150,181 +120,6 @@ - (void)scrollToNextRoomWithMissedNotifications } } -#pragma mark - Navigation - -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender -{ - [super prepareForSegue:segue sender:sender]; - - UIViewController *pushedViewController = [segue destinationViewController]; - - if ([[segue identifier] isEqualToString:@"presentDirectoryServerPicker"]) - { - UINavigationController *pushedNavigationViewController = (UINavigationController*)pushedViewController; - DirectoryServerPickerViewController* directoryServerPickerViewController = (DirectoryServerPickerViewController*)pushedNavigationViewController.viewControllers.firstObject; - - MXKDirectoryServersDataSource *directoryServersDataSource = [[MXKDirectoryServersDataSource alloc] initWithMatrixSession:recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; - [directoryServersDataSource finalizeInitialization]; - - // Add directory servers from the app settings - directoryServersDataSource.roomDirectoryServers = BuildSettings.publicRoomsDirectoryServers; - - __weak typeof(self) weakSelf = self; - - [directoryServerPickerViewController displayWithDataSource:directoryServersDataSource onComplete:^(id cellData) { - if (weakSelf && cellData) - { - typeof(self) self = weakSelf; - - // Use the selected directory server - if (cellData.thirdPartyProtocolInstance) - { - self->recentsDataSource.publicRoomsDirectoryDataSource.thirdpartyProtocolInstance = cellData.thirdPartyProtocolInstance; - } - else if (cellData.homeserver) - { - self->recentsDataSource.publicRoomsDirectoryDataSource.includeAllNetworks = cellData.includeAllNetworks; - self->recentsDataSource.publicRoomsDirectoryDataSource.homeserver = cellData.homeserver; - } - - // Refresh data - [self addSpinnerFooterView]; - - [self->recentsDataSource.publicRoomsDirectoryDataSource paginate:^(NSUInteger roomsAdded) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // The table view is automatically filled - [self removeSpinnerFooterView]; - - // Make the directory section appear full-page - [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:self->recentsDataSource.directorySection] atScrollPosition:UITableViewScrollPositionTop animated:YES]; - } - - } failure:^(NSError *error) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self removeSpinnerFooterView]; - } - }]; - } - }]; - - // Hide back button title - pushedViewController.navigationController.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; - } -} - -#pragma mark - UITableView delegate - -- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section -{ - if (section == recentsDataSource.directorySection) - { - // Let the recents dataSource provide the height of this section header - return [recentsDataSource heightForHeaderInSection:section]; - } - - return [super tableView:tableView heightForHeaderInSection:section]; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == recentsDataSource.directorySection) - { - // Sanity check - MXPublicRoom *publicRoom = [recentsDataSource.publicRoomsDirectoryDataSource roomAtIndexPath:indexPath]; - if (publicRoom) - { - [self openPublicRoomAtIndexPath:indexPath]; - } - } - else - { - [super tableView:tableView didSelectRowAtIndexPath:indexPath]; - } -} - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView -{ - // Trigger inconspicuous pagination on directy when user scrolls down - if ((scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.size.height) < 300) - { - [self triggerDirectoryPagination]; - } - - [super scrollViewDidScroll:scrollView]; -} - -#pragma mark - Private methods - -- (void)openPublicRoomAtIndexPath:(NSIndexPath *)indexPath -{ - MXPublicRoom *publicRoom = [recentsDataSource.publicRoomsDirectoryDataSource roomAtIndexPath:indexPath]; - - [self openPublicRoom:publicRoom]; -} - -- (void)triggerDirectoryPagination -{ - if (!recentsDataSource - || recentsDataSource.state == MXKDataSourceStateUnknown - || recentsDataSource.publicRoomsDirectoryDataSource.hasReachedPaginationEnd - || footerSpinnerView) - { - // We are not yet ready or being killed or we got all public rooms or we are already paginating - // Do nothing - return; - } - - [self addSpinnerFooterView]; - - [recentsDataSource.publicRoomsDirectoryDataSource paginate:^(NSUInteger roomsAdded) { - - // The table view is automatically filled - [self removeSpinnerFooterView]; - - } failure:^(NSError *error) { - - [self removeSpinnerFooterView]; - }]; -} - -- (void)addSpinnerFooterView -{ - if (!footerSpinnerView) - { - UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; - spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f); - CGRect frame = spinner.frame; - frame.size.height = 80; // 80 * 0.75 = 60 - spinner.bounds = frame; - - spinner.color = [UIColor darkGrayColor]; - spinner.hidesWhenStopped = NO; - spinner.backgroundColor = [UIColor clearColor]; - [spinner startAnimating]; - - // No need to manage constraints here, iOS defines them - self.recentsTableView.tableFooterView = footerSpinnerView = spinner; - } -} - -- (void)removeSpinnerFooterView -{ - if (footerSpinnerView) - { - footerSpinnerView = nil; - - // Hide line separators of empty cells - self.recentsTableView.tableFooterView = [[UIView alloc] init];; - } -} - #pragma mark - Empty view management - (void)updateEmptyView @@ -362,7 +157,6 @@ - (BOOL)shouldShowEmptyView - (NSUInteger)totalItemCounts { return recentsDataSource.conversationCellDataArray.count - + recentsDataSource.publicRoomsDirectoryDataSource.roomsCount + recentsDataSource.invitesCellDataArray.count; } From ce88d3b7e2f36c101985cc88b7376e1591e60784 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 7 Jul 2021 14:31:52 +0100 Subject: [PATCH 045/125] Remove custom section header for room directory from RecentsDataSource. --- .../Recents/DataSources/RecentsDataSource.m | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index a28236c04f..53415b066c 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -37,7 +37,6 @@ #define RECENTSDATASOURCE_SECTION_PEOPLE 0x40 #define RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT 30.0 -#define RECENTSDATASOURCE_DIRECTORY_SECTION_HEADER_HEIGHT 65.0 NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSourceTapOnDirectoryServerChange"; @@ -565,12 +564,6 @@ - (CGFloat)heightForHeaderInSection:(NSInteger)section { return 0.0; } - else if (section == directorySection - && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY) - && BuildSettings.publicRoomsAllowServerChange) - { - return RECENTSDATASOURCE_DIRECTORY_SECTION_HEADER_HEIGHT; - } return RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; } @@ -816,57 +809,6 @@ - (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame [sectionHeader addSubview:headerLabel]; sectionHeader.headerLabel = headerLabel; - if (section == directorySection - && _recentsDataSourceMode == RecentsDataSourceModeRooms - && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY) - && BuildSettings.publicRoomsAllowServerChange) - { - if (!directorySectionContainer) - { - directorySectionContainer = [[DirectorySectionHeaderContainerView alloc] initWithFrame:CGRectZero]; - directorySectionContainer.backgroundColor = [UIColor clearColor]; - - // Add the "Network" label at the left - networkLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 30)]; - networkLabel.font = [UIFont systemFontOfSize:16.0]; - networkLabel.text = NSLocalizedStringFromTable(@"room_recents_directory_section_network", @"Vector", nil); - [directorySectionContainer addSubview:networkLabel]; - directorySectionContainer.networkLabel = networkLabel; - - // Add label for selected directory server - directoryServerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 30)]; - directoryServerLabel.font = [UIFont systemFontOfSize:16.0]; - directoryServerLabel.textAlignment = NSTextAlignmentRight; - [directorySectionContainer addSubview:directoryServerLabel]; - directorySectionContainer.directoryServerLabel = directoryServerLabel; - - // Chevron - UIImageView *chevronImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 6, 12)]; - chevronImageView.contentMode = UIViewContentModeScaleAspectFit; - chevronImageView.image = [UIImage imageNamed:@"disclosure_icon"]; - chevronImageView.tintColor = ThemeService.shared.theme.textSecondaryColor; - [directorySectionContainer addSubview:chevronImageView]; - directorySectionContainer.disclosureView = chevronImageView; - - // Set a tap listener on all the container - UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onDirectoryServerPickerTap:)]; - [tapGesture setNumberOfTouchesRequired:1]; - [tapGesture setNumberOfTapsRequired:1]; - [directorySectionContainer addGestureRecognizer:tapGesture]; - } - - // Apply the current UI theme. - networkLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - directoryServerLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - - // Set the current directory server name - directoryServerLabel.text = _publicRoomsDirectoryDataSource.directoryServerDisplayname; - - // Add the check box container - [sectionHeader addSubview:directorySectionContainer]; - sectionHeader.bottomView = directorySectionContainer; - } - return sectionHeader; } From 7ae39531674cd0b5cb4b7f2e22358c355dedd59a Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 7 Jul 2021 15:05:40 +0100 Subject: [PATCH 046/125] Remove uninitialised header views. --- Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 53415b066c..0e2f14ea38 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -19,7 +19,6 @@ #import "RecentCellData.h" #import "SectionHeaderView.h" -#import "DirectorySectionHeaderContainerView.h" #import "ThemeService.h" @@ -47,10 +46,6 @@ @interface RecentsDataSource() *roomTagsListenerByUserId; // Timer to not refresh publicRoomsDirectoryDataSource on every keystroke. From 02244bfcf537f192ef8db5515ce146932bc96b7a Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 7 Jul 2021 17:37:33 +0100 Subject: [PATCH 047/125] Remove headers from RoomsViewController. --- Riot/Modules/Rooms/RoomsViewController.m | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index b3c820b730..979b30610c 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -58,8 +58,6 @@ - (void)viewDidLoad plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"rooms_floating_action"] target:self action:@selector(onPlusButtonPressed)]; - - self.enableStickyHeaders = YES; } - (void)viewWillAppear:(BOOL)animated @@ -96,17 +94,17 @@ - (void)refreshCurrentSelectedCell:(BOOL)forceVisible [super refreshCurrentSelectedCell:forceVisible]; } -- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section +- (void)onPlusButtonPressed { - CGRect frame = [tableView rectForHeaderInSection:section]; - frame.size.height = self.stickyHeaderHeight; - - return [recentsDataSource viewForHeaderInSection:section withFrame:frame]; + [self showRoomDirectory]; } -- (void)onPlusButtonPressed +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - [self showRoomDirectory]; + // Hide the header to merge Invites and Rooms into a single list. + return 0.0; } #pragma mark - From f6ed61d3342f243a46bf75888dc840d2f4643129 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 8 Jul 2021 09:51:50 +0100 Subject: [PATCH 048/125] Update CHANGES.rst. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 486c751b3c..f84e39fca7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changes to be released in next version * 🙌 Improvements - * + * Remove the directory section from the Rooms tab. 🐛 Bugfix * VoIP: Do not present ended calls. From c299e62fa02e45292580cce8826df5ae1d713e59 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 9 Jul 2021 12:36:02 +0100 Subject: [PATCH 049/125] Remove room_recents_directory_section_network localization string. --- Riot/Assets/bg.lproj/Vector.strings | 1 - Riot/Assets/ca.lproj/Vector.strings | 1 - Riot/Assets/cy.lproj/Vector.strings | 1 - Riot/Assets/de.lproj/Vector.strings | 1 - Riot/Assets/en.lproj/Vector.strings | 1 - Riot/Assets/eo.lproj/Vector.strings | 1 - Riot/Assets/es.lproj/Vector.strings | 1 - Riot/Assets/et.lproj/Vector.strings | 1 - Riot/Assets/eu.lproj/Vector.strings | 1 - Riot/Assets/fr.lproj/Vector.strings | 1 - Riot/Assets/hu.lproj/Vector.strings | 1 - Riot/Assets/is.lproj/Vector.strings | 1 - Riot/Assets/it.lproj/Vector.strings | 1 - Riot/Assets/ja.lproj/Vector.strings | 1 - Riot/Assets/kab.lproj/Vector.strings | 1 - Riot/Assets/nb-NO.lproj/Vector.strings | 1 - Riot/Assets/nl.lproj/Vector.strings | 1 - Riot/Assets/pl.lproj/Vector.strings | 1 - Riot/Assets/pt_BR.lproj/Vector.strings | 1 - Riot/Assets/ru.lproj/Vector.strings | 1 - Riot/Assets/sq.lproj/Vector.strings | 1 - Riot/Assets/sv.lproj/Vector.strings | 1 - Riot/Assets/vi.lproj/Vector.strings | 1 - Riot/Assets/zh_Hans.lproj/Vector.strings | 1 - Riot/Assets/zh_Hant.lproj/Vector.strings | 1 - Riot/Generated/Strings.swift | 4 ---- 26 files changed, 29 deletions(-) diff --git a/Riot/Assets/bg.lproj/Vector.strings b/Riot/Assets/bg.lproj/Vector.strings index d9fa91f5a9..04a6695243 100644 --- a/Riot/Assets/bg.lproj/Vector.strings +++ b/Riot/Assets/bg.lproj/Vector.strings @@ -106,7 +106,6 @@ // Room recents "room_recents_directory_section" = "ДИРЕКТОРИЯ СЪС СТАИ"; "room_creation_invite_another_user" = "Търси потребител по ID, име, имейл"; -"room_recents_directory_section_network" = "Мрежа"; "room_recents_favourites_section" = "ЛЮБИМИ"; "room_recents_people_section" = "ХОРА"; "room_recents_conversations_section" = "СТАИ"; diff --git a/Riot/Assets/ca.lproj/Vector.strings b/Riot/Assets/ca.lproj/Vector.strings index f11645924a..75c7c7fe20 100644 --- a/Riot/Assets/ca.lproj/Vector.strings +++ b/Riot/Assets/ca.lproj/Vector.strings @@ -106,7 +106,6 @@ "room_creation_invite_another_user" = "Cerca / convida per l'identificador d'usuari, nom o correu electrònic"; // Room recents "room_recents_directory_section" = "Directori de Sales"; -"room_recents_directory_section_network" = "Xarxa"; "room_recents_favourites_section" = "Favorits"; "room_recents_people_section" = "Contactes"; "room_recents_conversations_section" = "Sales"; diff --git a/Riot/Assets/cy.lproj/Vector.strings b/Riot/Assets/cy.lproj/Vector.strings index e1524bbc8d..868341391d 100644 --- a/Riot/Assets/cy.lproj/Vector.strings +++ b/Riot/Assets/cy.lproj/Vector.strings @@ -130,7 +130,6 @@ "room_creation_error_invite_user_by_email_without_identity_server" = "Nid oes unrhyw weinydd adnabod wedi'i osod felly ni allwch ychwanegu cyfranogwr gydag e-bost."; // Room recents "room_recents_directory_section" = "CYFEIRIADUR YSTAFELLOEDD"; -"room_recents_directory_section_network" = "Rhwydwaith"; "room_recents_favourites_section" = "FFEFRYNAU"; "room_recents_people_section" = "POBL"; "room_recents_conversations_section" = "YSTAFELLOEDD"; diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 52afbc691a..d58cacb4b9 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -79,7 +79,6 @@ "room_creation_wait_for_creation" = "Es wird gerade schon ein Raum erstellt. Bitte warten."; // Room recents "room_recents_directory_section" = "RAUM VERZEICHNIS"; -"room_recents_directory_section_network" = "Netzwerk"; "room_recents_favourites_section" = "FAVORITEN"; "room_recents_people_section" = "PERSONEN"; "room_recents_conversations_section" = "RÄUME"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 234022cf28..5585055fbe 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -182,7 +182,6 @@ // Room recents "room_recents_directory_section" = "ROOM DIRECTORY"; -"room_recents_directory_section_network" = "Network"; "room_recents_favourites_section" = "FAVOURITES"; "room_recents_people_section" = "PEOPLE"; "room_recents_conversations_section" = "ROOMS"; diff --git a/Riot/Assets/eo.lproj/Vector.strings b/Riot/Assets/eo.lproj/Vector.strings index 8a4575e645..bf7636ecb4 100644 --- a/Riot/Assets/eo.lproj/Vector.strings +++ b/Riot/Assets/eo.lproj/Vector.strings @@ -244,7 +244,6 @@ "room_recents_server_notice_section" = "SISTEMAJ AVERTOJ"; "room_recents_low_priority_section" = "MALALTA PRIORITATO"; "room_recents_favourites_section" = "ELSTARIGITAJ"; -"room_recents_directory_section_network" = "Reto"; // Room recents "room_recents_directory_section" = "KATALOGO DE ĈAMBROJ"; diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index c14cdda6b0..216722a928 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -144,7 +144,6 @@ "room_creation_invite_another_user" = "Buscar / invitar por ID de Usuario, Nombre o correo electrónico"; // Room recents "room_recents_directory_section" = "DIRECTORIO DE SALAS"; -"room_recents_directory_section_network" = "Red"; "room_recents_favourites_section" = "FAVORITOS"; "room_recents_people_section" = "PERSONAS"; "room_recents_conversations_section" = "SALAS"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 32ede1f7f3..904fed413f 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -218,7 +218,6 @@ "auth_login_single_sign_on" = "Logi sisse"; // Room recents "room_recents_directory_section" = "JUTUTUBADE LOEND"; -"room_recents_directory_section_network" = "Võrk"; "room_recents_favourites_section" = "LEMMIKUD"; "room_recents_people_section" = "INIMESED"; "room_recents_conversations_section" = "JUTUTOAD"; diff --git a/Riot/Assets/eu.lproj/Vector.strings b/Riot/Assets/eu.lproj/Vector.strings index 38a6e4d340..9278d4a1d6 100644 --- a/Riot/Assets/eu.lproj/Vector.strings +++ b/Riot/Assets/eu.lproj/Vector.strings @@ -238,7 +238,6 @@ "room_creation_invite_another_user" = "Bilatu / gonbidatu erabiltzaile ID-a, izena edo e-maila erabiliz"; // Room recents "room_recents_directory_section" = "GELEN DIREKTORIOA"; -"room_recents_directory_section_network" = "Sarea"; "room_recents_favourites_section" = "GOGOKOAK"; "room_recents_invites_section" = "GONBIDAPENAK"; // People tab diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index f78010dfe2..6e68a7a94d 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -97,7 +97,6 @@ "room_creation_invite_another_user" = "Rechercher/inviter par identifiant, nom ou e-mail"; // Room recents "room_recents_directory_section" = "RÉPERTOIRE DES SALONS"; -"room_recents_directory_section_network" = "Réseau"; "room_recents_favourites_section" = "FAVORIS"; "room_recents_people_section" = "PERSONNES"; "room_recents_conversations_section" = "SALONS"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 718f7de9bc..0d516eb935 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -108,7 +108,6 @@ "room_creation_invite_another_user" = "Keresés / meghívás felhasználói azonosítás, név vagy e-mail cím alapján"; // Room recents "room_recents_directory_section" = "SZOBA KÖNYVTÁR"; -"room_recents_directory_section_network" = "Hálózat"; "room_recents_favourites_section" = "KEDVENCEK"; "room_recents_people_section" = "EMBEREK"; "room_recents_conversations_section" = "SZOBÁK"; diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index f6fcd78c39..bd4577fdac 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -63,7 +63,6 @@ "room_creation_appearance_name" = "Heiti"; "room_creation_privacy" = "Meðferð persónuupplýsinga"; "room_creation_make_private" = "Gera einka"; -"room_recents_directory_section_network" = "Netkerfi"; "room_recents_favourites_section" = "Eftirlæti"; "room_recents_people_section" = "FÓLK"; "room_recents_conversations_section" = "SPJALLRÁSIR"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index b6a2f0829b..4159607641 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -112,7 +112,6 @@ "room_creation_invite_another_user" = "Cerca / invita per ID utente, nome o email"; // Room recents "room_recents_directory_section" = "ELENCO STANZE"; -"room_recents_directory_section_network" = "Rete"; "room_recents_favourites_section" = "PREFERITI"; "room_recents_people_section" = "CHAT DIRETTE"; "room_recents_conversations_section" = "STANZE"; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index a3d3a68e7a..63fb432a94 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -105,7 +105,6 @@ "room_creation_invite_another_user" = "ユーザID, 表示名, 電子メールで検索と招待"; // Room recents "room_recents_directory_section" = "部屋一覧"; -"room_recents_directory_section_network" = "通信回線"; "room_recents_favourites_section" = "お気に入り"; "room_recents_people_section" = "対話"; "room_recents_conversations_section" = "部屋"; diff --git a/Riot/Assets/kab.lproj/Vector.strings b/Riot/Assets/kab.lproj/Vector.strings index 812f4def9c..1fa265a061 100644 --- a/Riot/Assets/kab.lproj/Vector.strings +++ b/Riot/Assets/kab.lproj/Vector.strings @@ -383,7 +383,6 @@ "room_recents_no_conversation" = "Ulac tixxamin"; "room_recents_conversations_section" = "TIXXAMIN"; "room_recents_people_section" = "IMDANEN"; -"room_recents_directory_section_network" = "Aẓeṭṭa"; "room_creation_make_private" = "Err-it d uslig"; "room_creation_privacy" = "Tabaḍnit"; "room_creation_appearance_name" = "Isem"; diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index c47bf5f512..1a10b6f275 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -64,7 +64,6 @@ "room_creation_appearance" = "Utseende"; "room_creation_appearance_name" = "Navn"; "room_creation_privacy" = "Personvern"; -"room_recents_directory_section_network" = "Nettverk"; "room_recents_create_empty_room" = "Opprett et rom"; "room_recents_join_room" = "Bli med i rommet"; "room_recents_join_room_title" = "Bli med i et rom"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 4b144ba889..9c3048ec6d 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -112,7 +112,6 @@ "room_creation_invite_another_user" = "Zoeken/uitnodigen met gebruikers-ID, naam of e-mailadres"; // Room recents "room_recents_directory_section" = "GESPREKSCATALOGUS"; -"room_recents_directory_section_network" = "Netwerk"; "room_recents_favourites_section" = "FAVORIETEN"; "room_recents_people_section" = "PERSONEN"; "room_recents_conversations_section" = "GESPREKKEN"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 9e57955c23..9fc3c92a91 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -86,7 +86,6 @@ "room_creation_appearance" = "Wygląd"; "room_creation_appearance_name" = "Nazwa"; "room_creation_privacy" = "Prywatność"; -"room_recents_directory_section_network" = "Sieć"; "room_recents_favourites_section" = "ULUBIONE"; "room_recents_people_section" = "OSOBY"; "room_recents_conversations_section" = "POKOJE"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 9a9f7396b9..a253b42678 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -101,7 +101,6 @@ "room_creation_make_private" = "Fazer privado"; "room_creation_wait_for_creation" = "Uma sala já está sendo criada. Por favor espere."; "room_creation_invite_another_user" = "Buscar / convidar por ID de usuária(o), Nome ou email"; -"room_recents_directory_section_network" = "Rede"; "room_recents_favourites_section" = "FAVORITOS"; "room_recents_people_section" = "PESSOAS"; "room_recents_conversations_section" = "SALAS"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 4b717f3f80..0ad6dbef63 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -98,7 +98,6 @@ "room_creation_invite_another_user" = "Поиск / приглашение по идентификатору пользователя, имени или адресу электронной почты"; // Room recents "room_recents_directory_section" = "КАТАЛОГ КОМНАТ"; -"room_recents_directory_section_network" = "Сеть"; "room_recents_favourites_section" = "ИЗБРАННЫЕ"; "room_recents_people_section" = "ЛЮДИ"; "room_recents_conversations_section" = "КОМНАТЫ"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 0794ad0977..cac4cc9861 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -84,7 +84,6 @@ "room_creation_make_private" = "Bëje private"; // Room recents "room_recents_directory_section" = "DREJTORI DHOMASH"; -"room_recents_directory_section_network" = "Rrjet"; "room_recents_people_section" = "PERSONA"; "room_recents_conversations_section" = "DHOMA"; "room_recents_no_conversation" = "S’ka dhoma"; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 14a8abd727..1bbec2ef64 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -100,7 +100,6 @@ "room_creation_make_private" = "Gör privat"; "room_creation_wait_for_creation" = "Ett rum håller redan på att skapas. Vänligen vänta."; "room_creation_invite_another_user" = "Sök / bjud in efter användar-ID, namn eller e-postadress"; -"room_recents_directory_section_network" = "Nätverk"; "room_recents_favourites_section" = "FAVORITER"; "room_recents_people_section" = "PERSONER"; "room_recents_conversations_section" = "RUM"; diff --git a/Riot/Assets/vi.lproj/Vector.strings b/Riot/Assets/vi.lproj/Vector.strings index 9b2d328c4a..88fc75f851 100644 --- a/Riot/Assets/vi.lproj/Vector.strings +++ b/Riot/Assets/vi.lproj/Vector.strings @@ -105,7 +105,6 @@ "room_creation_invite_another_user" = "Tìm / mời bằng ID người dùng, tên hoặc email"; // Room recents "room_recents_directory_section" = "DANH MỤC PHÒNG"; -"room_recents_directory_section_network" = "Mạng"; "room_recents_favourites_section" = "YÊU THÍCH"; "room_recents_people_section" = "DANH BẠ"; "room_recents_conversations_section" = "PHÒNG"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 43a6650812..da074b7677 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -96,7 +96,6 @@ "room_creation_invite_another_user" = "通过用户 ID、名称或电子邮件进行搜索/邀请"; // Room recents "room_recents_directory_section" = "聊天室目录"; -"room_recents_directory_section_network" = "网络"; "room_recents_favourites_section" = "收藏夹"; "room_recents_people_section" = "联系人"; "room_recents_conversations_section" = "聊天室"; diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 6857222931..de6fbf1ab1 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -184,7 +184,6 @@ "room_creation_invite_another_user" = "透過使用者ID、名稱、電子郵件地址來搜尋/邀請"; // Room recents "room_recents_directory_section" = "聊天室目錄"; -"room_recents_directory_section_network" = "網路"; "room_recents_favourites_section" = "收藏夾"; "room_recents_people_section" = "聯絡人"; "room_recents_conversations_section" = "聊天室"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ba05c0233b..1cfa73d7fc 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3314,10 +3314,6 @@ internal enum VectorL10n { internal static var roomRecentsDirectorySection: String { return VectorL10n.tr("Vector", "room_recents_directory_section") } - /// Network - internal static var roomRecentsDirectorySectionNetwork: String { - return VectorL10n.tr("Vector", "room_recents_directory_section_network") - } /// FAVOURITES internal static var roomRecentsFavouritesSection: String { return VectorL10n.tr("Vector", "room_recents_favourites_section") From 18e1c115b9dd1efe1844fd816c00ec12a635cfeb Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 9 Jul 2021 14:22:33 +0100 Subject: [PATCH 050/125] Remove headers from PeopleViewController. --- Riot/Modules/People/PeopleViewController.m | 32 ++-------------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 79cbf86e7e..e2e839d997 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -74,8 +74,6 @@ - (void)viewDidLoad // Change the table data source. It must be the people view controller itself. self.recentsTableView.dataSource = self; - - self.enableStickyHeaders = YES; } - (void)didReceiveMemoryWarning @@ -181,24 +179,12 @@ - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *) - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - // FIXME: Should this need to check the section? - if (section >= directRoomsSectionNumber) - { - return 0.0; - } - - return [super tableView:tableView heightForHeaderInSection:section]; + return 0.0; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { - // FIXME: Should this need to check the section? - if (section >= directRoomsSectionNumber) - { - return nil; - } - - return [super tableView:tableView viewForHeaderInSection:section]; + return nil; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath @@ -226,20 +212,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath #pragma mark - Override RecentsViewController -- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section -{ - // FIXME: Should this need to check the section? - if (section >= directRoomsSectionNumber || recentsDataSource == nil) - { - return nil; - } - - CGRect frame = [tableView rectForHeaderInSection:section]; - frame.size.height = self.stickyHeaderHeight; - - return [recentsDataSource viewForStickyHeaderInSection:section withFrame:frame]; -} - - (void)refreshCurrentSelectedCell:(BOOL)forceVisible { // Check whether the recents data source is correctly configured. From 9ba4066257bdeb0175c1b5b5c36f2d480e6fa8ea Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 9 Jul 2021 15:00:03 +0100 Subject: [PATCH 051/125] Add decryptNotificationsByDefault build setting. --- Config/BuildSettings.swift | 3 +++ Riot/Modules/Application/LegacyAppDelegate.m | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 3cfcf65dc8..76cf11fd0c 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -308,6 +308,9 @@ final class BuildSettings: NSObject { static let messageDetailsAllowCopyMedia: Bool = true static let messageDetailsAllowPasteMedia: Bool = true + // MARK: - Notifications + static let decryptNotificationsByDefault: Bool = true + // MARK: - HTTP /// Additional HTTP headers will be sent by all requests. Not recommended to use request-specific headers, like `Authorization`. /// Empty dictionary by default. diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index be5ad0b46d..049b830e7e 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -4210,7 +4210,7 @@ - (void)setupUserDefaults // Show encrypted message notification content by default. if (!RiotSettings.shared.isShowDecryptedContentInNotificationsHasBeenSetOnce) { - RiotSettings.shared.showDecryptedContentInNotifications = YES; + RiotSettings.shared.showDecryptedContentInNotifications = BuildSettings.decryptNotificationsByDefault; } } From 65415c48efd20f20cfcc01d931dbf77d77366c26 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Sat, 10 Jul 2021 00:16:23 +0200 Subject: [PATCH 052/125] #4090 - Update after review --- .../Contents.json | 3 ++ .../Contents.json | 3 ++ Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 2 +- .../VoiceMessageController.swift | 24 ++++++++++++-- .../VoiceMessagePlaybackView.swift | 3 +- .../VoiceMessageToolbarView.swift | 32 ++++++++++++++++++- 7 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json index 21dd49f04a..bfc1545dbf 100644 --- a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json index 2edca40322..3c88e866ca 100644 --- a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index eef3e9aa36..b615b72d6a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1671,6 +1671,6 @@ Tap the + to start adding people."; // Mark: - Voice Messages -"voice_message_release_to_send" = "Release to send"; +"voice_message_release_to_send" = "Hold to record, release to send"; "voice_message_remaining_recording_time" = "%@s left"; "voice_message_stop_locked_mode_recording" = "Tap on the wavelength to stop and playback"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 57f016874d..2f9ab124ab 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4838,7 +4838,7 @@ internal enum VectorL10n { internal static var voice: String { return VectorL10n.tr("Vector", "voice") } - /// Release to send + /// Hold to record, release to send internal static var voiceMessageReleaseToSend: String { return VectorL10n.tr("Vector", "voice_message_release_to_send") } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index a574b8bb5d..3f2f5f793f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -48,6 +48,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private var audioSamples: [Float] = [] private var isInLockedMode: Bool = false + private var notifiedRemainingTime = false @objc public weak var delegate: VoiceMessageControllerDelegate? @@ -82,7 +83,17 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, delegate?.voiceMessageControllerDidRequestMicrophonePermission(self) return } - + + // Haptic are not played during record on iOS by default. This fix works + // only since iOS 13. A workaround for iOS 12 and earlier would be to + // dispatch after at least 100ms recordWithOuputURL call + if #available(iOS 13.0, *) { + try? AVAudioSession.sharedInstance().setCategory(.playAndRecord) + try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) + } + + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") @@ -133,6 +144,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - AudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { + notifiedRemainingTime = false updateUI() } @@ -277,7 +289,15 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, if isRecording { if currentTime >= Constants.maximumAudioRecordingDuration - Constants.maximumAudioRecordingLengthReachedThreshold { - details.toastMessage = VectorL10n.voiceMessageRemainingRecordingTime(String(Constants.maximumAudioRecordingLengthReachedThreshold)) + + if !self.notifiedRemainingTime { + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + } + + notifiedRemainingTime = true + + let remainingTime = ceil(Constants.maximumAudioRecordingDuration - currentTime) + details.toastMessage = VectorL10n.voiceMessageRemainingRecordingTime(String(remainingTime)) } else { details.toastMessage = (isInLockedMode ? VectorL10n.voiceMessageStopLockedModeRecording : VectorL10n.voiceMessageReleaseToSend) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index c749bcd47f..36b2e5248e 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -103,7 +103,8 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { } self.backgroundColor = theme.colors.background - playButton.backgroundColor = theme.colors.separator + playButton.backgroundColor = theme.colors.background + playButton.tintColor = theme.colors.secondaryContent backgroundView.backgroundColor = theme.colors.quinaryContent _waveformView.primarylineColor = theme.colors.quarterlyContent _waveformView.secondaryLineColor = theme.colors.secondaryContent diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 9a37e99179..d29dbeebb7 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -50,6 +50,7 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture static let lockModeTransitionAnimationDuration: TimeInterval = 0.5 static let panDirectionChangeThreshold: CGFloat = 20.0 static let toastContainerCornerRadii: CGFloat = 8.0 + static let toastDisplayTimeout: TimeInterval = 5.0 } @IBOutlet private var backgroundView: UIView! @@ -309,15 +310,44 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture } } + private var toastIdleTimer: Timer? + private var lastUIState: VoiceMessageToolbarViewUIState = .idle + private func updateToastNotificationsWithDetails(_ details: VoiceMessageToolbarViewDetails, animated: Bool = true) { + + guard self.toastNotificationLabel.text != details.toastMessage || lastUIState != details.state else { + return + } + + lastUIState = details.state + let shouldShowNotification = details.state != .idle && details.toastMessage != nil + let requiredAlpha: CGFloat = shouldShowNotification ? 1.0 : 0.0 + + toastIdleTimer?.invalidate() + toastIdleTimer = nil if shouldShowNotification { self.toastNotificationLabel.text = details.toastMessage } UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0)) { - self.toastNotificationContainerView.alpha = (shouldShowNotification ? 1.0 : 0.0) + self.toastNotificationContainerView.alpha = requiredAlpha + } + + if shouldShowNotification { + toastIdleTimer = Timer.scheduledTimer(withTimeInterval: Constants.toastDisplayTimeout, repeats: false) { [weak self] timer in + guard let self = self else { + return + } + + self.toastIdleTimer?.invalidate() + self.toastIdleTimer = nil + + UIView.animate(withDuration: Constants.animationDuration) { + self.toastNotificationContainerView.alpha = 0 + } + } } } From 7d1f33c9ed8c1cff7679c83cd25b6d22c090a9d9 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jul 2021 10:18:01 +0300 Subject: [PATCH 053/125] #4094 - Reduced the minimum recording duration to 1 second. --- Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 3f2f5f793f..03d5140b05 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -29,7 +29,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, static let maximumAudioRecordingDuration: TimeInterval = 120.0 static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0 static let elapsedTimeFormat = "m:ss" - static let minimumRecordingDuration = 2.0 + static let minimumRecordingDuration = 1.0 } private static let timeFormatter: DateFormatter = { From 555fa5d91bd0ede618e4dcebce90e3dd28e42aa2 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jul 2021 11:53:02 +0300 Subject: [PATCH 054/125] #4094 - Updated english NSMicrophoneUsageDescription. --- Riot/Assets/en.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 6387f123d0..468d42cce9 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -17,7 +17,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "The camera is used to take photos and videos, make video calls."; "NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; -"NSMicrophoneUsageDescription" = "The microphone is used to take videos, make calls."; +"NSMicrophoneUsageDescription" = "Element needs to access your microphone to make and receive calls, take videos, and record voice messages."; "NSContactsUsageDescription" = "To discover contacts already using Matrix, Element can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; From 9030f02de6e72898133f99bc83306a684d223153 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jul 2021 12:18:49 +0300 Subject: [PATCH 055/125] #4094 - Disable message replies while recording audio messages. --- Riot/Modules/Room/RoomViewController.m | 2 +- Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 3338f979be..b6367ad507 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5831,7 +5831,7 @@ - (RoomContextualMenuItem *)replyMenuItemWithEvent:(MXEvent*)event MXWeakify(self); RoomContextualMenuItem *replyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReply]; - replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId]; + replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio; replyMenuItem.action = ^{ MXStrongifyAndReturnIfNil(self); diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 03d5140b05..dd641929bc 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -52,6 +52,10 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, @objc public weak var delegate: VoiceMessageControllerDelegate? + @objc public var isRecordingAudio: Bool { + return mediaServiceProvider.audioRecorder.isRecording || isInLockedMode + } + @objc public var voiceMessageToolbarView: UIView { return _voiceMessageToolbarView } From 1a2a434d9dabf08caecdd9d6833abf02e2e77365 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jul 2021 15:30:48 +0300 Subject: [PATCH 056/125] #4545 - Switch back to using multiple audio player instances, allow pausing when starting a new player. --- .../VoiceMessageController.swift | 64 +++++++++++-------- .../VoiceMessageMediaServiceProvider.swift | 49 +++++++++++--- .../VoiceMessagePlaybackController.swift | 46 +++---------- 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index dd641929bc..4be0e2c192 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -44,6 +44,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private let _voiceMessageToolbarView: VoiceMessageToolbarView private var displayLink: CADisplayLink! + private var audioRecorder: VoiceMessageAudioRecorder? + + private var audioPlayer: VoiceMessageAudioPlayer? private var waveformAnalyser: WaveformAnalyzer? private var audioSamples: [Float] = [] @@ -53,7 +56,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, @objc public weak var delegate: VoiceMessageControllerDelegate? @objc public var isRecordingAudio: Bool { - return mediaServiceProvider.audioRecorder.isRecording || isInLockedMode + return audioRecorder?.isRecording ?? false || isInLockedMode } @objc public var voiceMessageToolbarView: UIView { @@ -101,8 +104,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - mediaServiceProvider.audioRecorder.registerDelegate(self) - mediaServiceProvider.audioRecorder.recordWithOuputURL(temporaryFileURL) + audioRecorder = mediaServiceProvider.audioRecorder() + audioRecorder?.registerDelegate(self) + audioRecorder?.recordWithOuputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -111,8 +115,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { isInLockedMode = false - mediaServiceProvider.audioRecorder.stopRecording() - deleteRecordingAtURL(mediaServiceProvider.audioRecorder.url) + audioRecorder?.stopRecording() + deleteRecordingAtURL(audioRecorder?.url) UINotificationFeedbackGenerator().notificationOccurred(.error) updateUI() } @@ -123,21 +127,21 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { - if mediaServiceProvider.audioPlayer.isPlaying { - mediaServiceProvider.audioPlayer.pause() + if audioPlayer?.isPlaying ?? false { + audioPlayer?.pause() } else { - mediaServiceProvider.audioPlayer.play() + audioPlayer?.play() } } func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) { - guard let url = mediaServiceProvider.audioRecorder.url else { + guard let url = audioRecorder?.url else { MXLog.error("Invalid audio recording URL") return } - mediaServiceProvider.audioPlayer.stop() - mediaServiceProvider.audioRecorder.stopRecording() + audioPlayer?.stop() + audioRecorder?.stopRecording() sendRecordingAtURL(url) @@ -191,23 +195,25 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private private func finishRecording() { - let recordDuration = mediaServiceProvider.audioRecorder.currentTime - mediaServiceProvider.audioRecorder.stopRecording() + let recordDuration = audioRecorder?.currentTime + audioRecorder?.stopRecording() - guard let url = mediaServiceProvider.audioRecorder.url else { + guard let url = audioRecorder?.url else { MXLog.error("Invalid audio recording URL") return } guard isInLockedMode else { - if recordDuration >= Constants.minimumRecordingDuration { + if recordDuration ?? 0 >= Constants.minimumRecordingDuration { sendRecordingAtURL(url) } return } - mediaServiceProvider.audioPlayer.registerDelegate(self) - mediaServiceProvider.audioPlayer.loadContentFromURL(url) + audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer?.registerDelegate(self) + audioPlayer?.loadContentFromURL(url) + audioSamples = [] updateUI() @@ -255,7 +261,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private func updateUI() { - let shouldUpdateFromAudioPlayer = isInLockedMode && !mediaServiceProvider.audioRecorder.isRecording + let shouldUpdateFromAudioPlayer = isInLockedMode && !(audioRecorder?.isRecording ?? false) if shouldUpdateFromAudioPlayer { updateUIFromAudioPlayer() @@ -265,7 +271,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioRecorder() { - let isRecording = mediaServiceProvider.audioRecorder.isRecording + let isRecording = audioRecorder?.isRecording ?? false displayLink.isPaused = !isRecording @@ -275,11 +281,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, padSamplesArrayToSize(requiredNumberOfSamples) } - let sample = mediaServiceProvider.audioRecorder.averagePowerForChannelNumber(0) + let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0 audioSamples.insert(sample, at: 0) audioSamples.removeLast() - let currentTime = mediaServiceProvider.audioRecorder.currentTime + let currentTime = audioRecorder?.currentTime ?? 0.0 if currentTime >= Constants.maximumAudioRecordingDuration { finishRecording() @@ -311,12 +317,16 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioPlayer() { - guard let url = mediaServiceProvider.audioPlayer.url else { + guard let audioPlayer = audioPlayer else { + return + } + + guard let url = audioPlayer.url else { MXLog.error("Invalid audio player url.") return } - displayLink.isPaused = !mediaServiceProvider.audioPlayer.isPlaying + displayLink.isPaused = !audioPlayer.isPlaying let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() if audioSamples.count != requiredNumberOfSamples && requiredNumberOfSamples > 0 { @@ -337,11 +347,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } var details = VoiceMessageToolbarViewDetails() - details.state = (mediaServiceProvider.audioRecorder.isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (mediaServiceProvider.audioPlayer.isPlaying ? mediaServiceProvider.audioPlayer.currentTime : mediaServiceProvider.audioPlayer.duration))) + details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) details.audioSamples = audioSamples - details.isPlaying = mediaServiceProvider.audioPlayer.isPlaying - details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) + details.isPlaying = audioPlayer.isPlaying + details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) _voiceMessageToolbarView.configureWithDetails(details) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 7d455c0a3d..f6b558af85 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -18,32 +18,63 @@ import Foundation @objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate { - let audioPlayer = VoiceMessageAudioPlayer() - var mediaIdentifier: String? - let audioRecorder = VoiceMessageAudioRecorder() - + private let audioPlayers: NSHashTable + private let audioRecorders: NSHashTable + @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() private override init() { - super.init() + audioPlayers = NSHashTable(options: .weakMemory) + audioRecorders = NSHashTable(options: .weakMemory) + } + + @objc func audioPlayer() -> VoiceMessageAudioPlayer { + let audioPlayer = VoiceMessageAudioPlayer() audioPlayer.registerDelegate(self) + audioPlayers.add(audioPlayer) + return audioPlayer + } + + @objc func audioRecorder() -> VoiceMessageAudioRecorder { + let audioRecorder = VoiceMessageAudioRecorder() audioRecorder.registerDelegate(self) + audioRecorders.add(audioRecorder) + return audioRecorder } @objc func stopAllServices() { - audioPlayer.stop() - audioRecorder.stopRecording() + stopAllServicesExcept(nil) } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { - audioRecorder.stopRecording() + stopAllServicesExcept(audioPlayer) } // MARK: - VoiceMessageAudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - audioPlayer.stop() + stopAllServicesExcept(audioRecorder) + } + + // MARK: - Private + + private func stopAllServicesExcept(_ service: AnyObject?) { + for audioPlayer in audioPlayers.allObjects { + if audioPlayer === service { + continue + } + + audioPlayer.pause() + } + + for audioRecoder in audioRecorders.allObjects { + if audioRecoder === service { + continue + } + + audioRecoder.stopRecording() + } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 57aa4bba8a..443dfa7f7c 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -37,7 +37,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess }() private let cacheManager: VoiceMessageAttachmentCacheManager - private let mediaServiceProvider: VoiceMessageMediaServiceProvider + + private let audioPlayer: VoiceMessageAudioPlayer private var displayLink: CADisplayLink! private var samples: [Float] = [] private var duration: TimeInterval = 0 @@ -46,9 +47,6 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess private var state: VoiceMessagePlaybackControllerState = .stopped { didSet { - if state == .stopped || state == .error { - mediaServiceProvider.audioPlayer.deregisterDelegate(self) - } updateUI() displayLink.isPaused = (state != .playing) } @@ -59,10 +57,11 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess init(mediaServiceProvider: VoiceMessageMediaServiceProvider, cacheManager: VoiceMessageAttachmentCacheManager) { self.cacheManager = cacheManager - self.mediaServiceProvider = mediaServiceProvider playbackView = VoiceMessagePlaybackView.loadFromNib() + audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer.registerDelegate(self) playbackView.delegate = self displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -83,29 +82,16 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess // MARK: - VoiceMessagePlaybackViewDelegate func voiceMessagePlaybackViewDidRequestPlaybackToggle() { - if mediaServiceProvider.mediaIdentifier == attachment?.eventId { - if mediaServiceProvider.audioPlayer.isPlaying { - mediaServiceProvider.audioPlayer.pause() - } else { - mediaServiceProvider.audioPlayer.registerDelegate(self) - mediaServiceProvider.audioPlayer.play() - } + if audioPlayer.isPlaying { + audioPlayer.pause() } else { - if let url = urlToLoad { - mediaServiceProvider.mediaIdentifier = attachment?.eventId - mediaServiceProvider.audioPlayer.registerDelegate(self) - mediaServiceProvider.audioPlayer.loadContentFromURL(url) - mediaServiceProvider.audioPlayer.play() - } + audioPlayer.play() } } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { - if audioPlayer.url != self.urlToLoad { - state = .stopped - } updateUI() } @@ -149,8 +135,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: mediaServiceProvider.audioPlayer.currentTime)) - details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) + details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) } details.loading = self.loading @@ -162,7 +148,6 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return } - mediaServiceProvider.audioPlayer.deregisterDelegate(self) self.state = .stopped self.loading = true self.samples = [] @@ -178,21 +163,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } self.loading = false - self.urlToLoad = result.1 + self.audioPlayer.loadContentFromURL(result.1) self.duration = result.2 self.samples = result.3 - if self.mediaServiceProvider.mediaIdentifier == self.attachment?.eventId { - self.mediaServiceProvider.audioPlayer.registerDelegate(self) - if self.mediaServiceProvider.audioPlayer.isPlaying { - self.state = .playing - } else if self.mediaServiceProvider.audioPlayer.currentTime > 0 { - self.state = .paused - } else { - self.state = .stopped - } - } - self.updateUI() case .failure: self.state = .error From ba54f91ffc5e7c2423403e66d2a9259fe6781059 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jul 2021 15:31:18 +0300 Subject: [PATCH 057/125] #4094 - Fix crash on concurrent access to waveform audio samples. --- .../Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift index c0abee8439..9a72c09021 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -82,7 +82,7 @@ class VoiceMessageWaveformView: UIView { // MARK: - Private private func computeWaveForm() { - renderingQueue.async { + renderingQueue.async { [samples] in // Capture the current samples as a way to provide atomicity let path = UIBezierPath() let drawMappingFactor = self.bounds.size.height @@ -92,7 +92,7 @@ class VoiceMessageWaveformView: UIView { var index = 0 while xOffset < self.bounds.width - self.lineWidth { - let sample = CGFloat(index >= self.samples.count ? 1 : self.samples[index]) + let sample = CGFloat(index >= samples.count ? 1 : samples[index]) let invertedDbSample = 1 - sample // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) let drawingAmplitude = max(minimumGraphAmplitude, invertedDbSample * drawMappingFactor) From a5136a38bba370dcfc16124ae0c3940143dd83e3 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jul 2021 15:50:44 +0300 Subject: [PATCH 058/125] #4094 - Fixed attachments caching layer not working accordingly. --- .../Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index d15a0c990d..7890fd3a03 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -126,6 +126,7 @@ class VoiceMessageAttachmentCacheManager { switch result { case .success: if let duration = try? result.get() { + self.durations[identifier] = duration sampleFileAtURL(newURL, duration: duration) } else { MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: Failed to retrieve media duration") From 1a5197c69a9caa0e961fd58939a76e4c4349468e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 13 Jul 2021 08:04:50 +0300 Subject: [PATCH 059/125] Revert "#4545 - Switch back to using multiple audio player instances, allow pausing when starting a new player." This reverts commit 1a2a434d9dabf08caecdd9d6833abf02e2e77365. --- .../VoiceMessageController.swift | 64 ++++++++----------- .../VoiceMessageMediaServiceProvider.swift | 49 +++----------- .../VoiceMessagePlaybackController.swift | 46 ++++++++++--- 3 files changed, 72 insertions(+), 87 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 4be0e2c192..dd641929bc 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -44,9 +44,6 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private let _voiceMessageToolbarView: VoiceMessageToolbarView private var displayLink: CADisplayLink! - private var audioRecorder: VoiceMessageAudioRecorder? - - private var audioPlayer: VoiceMessageAudioPlayer? private var waveformAnalyser: WaveformAnalyzer? private var audioSamples: [Float] = [] @@ -56,7 +53,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, @objc public weak var delegate: VoiceMessageControllerDelegate? @objc public var isRecordingAudio: Bool { - return audioRecorder?.isRecording ?? false || isInLockedMode + return mediaServiceProvider.audioRecorder.isRecording || isInLockedMode } @objc public var voiceMessageToolbarView: UIView { @@ -104,9 +101,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - audioRecorder = mediaServiceProvider.audioRecorder() - audioRecorder?.registerDelegate(self) - audioRecorder?.recordWithOuputURL(temporaryFileURL) + mediaServiceProvider.audioRecorder.registerDelegate(self) + mediaServiceProvider.audioRecorder.recordWithOuputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -115,8 +111,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { isInLockedMode = false - audioRecorder?.stopRecording() - deleteRecordingAtURL(audioRecorder?.url) + mediaServiceProvider.audioRecorder.stopRecording() + deleteRecordingAtURL(mediaServiceProvider.audioRecorder.url) UINotificationFeedbackGenerator().notificationOccurred(.error) updateUI() } @@ -127,21 +123,21 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { - if audioPlayer?.isPlaying ?? false { - audioPlayer?.pause() + if mediaServiceProvider.audioPlayer.isPlaying { + mediaServiceProvider.audioPlayer.pause() } else { - audioPlayer?.play() + mediaServiceProvider.audioPlayer.play() } } func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) { - guard let url = audioRecorder?.url else { + guard let url = mediaServiceProvider.audioRecorder.url else { MXLog.error("Invalid audio recording URL") return } - audioPlayer?.stop() - audioRecorder?.stopRecording() + mediaServiceProvider.audioPlayer.stop() + mediaServiceProvider.audioRecorder.stopRecording() sendRecordingAtURL(url) @@ -195,25 +191,23 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private private func finishRecording() { - let recordDuration = audioRecorder?.currentTime - audioRecorder?.stopRecording() + let recordDuration = mediaServiceProvider.audioRecorder.currentTime + mediaServiceProvider.audioRecorder.stopRecording() - guard let url = audioRecorder?.url else { + guard let url = mediaServiceProvider.audioRecorder.url else { MXLog.error("Invalid audio recording URL") return } guard isInLockedMode else { - if recordDuration ?? 0 >= Constants.minimumRecordingDuration { + if recordDuration >= Constants.minimumRecordingDuration { sendRecordingAtURL(url) } return } - audioPlayer = mediaServiceProvider.audioPlayer() - audioPlayer?.registerDelegate(self) - audioPlayer?.loadContentFromURL(url) - + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.loadContentFromURL(url) audioSamples = [] updateUI() @@ -261,7 +255,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private func updateUI() { - let shouldUpdateFromAudioPlayer = isInLockedMode && !(audioRecorder?.isRecording ?? false) + let shouldUpdateFromAudioPlayer = isInLockedMode && !mediaServiceProvider.audioRecorder.isRecording if shouldUpdateFromAudioPlayer { updateUIFromAudioPlayer() @@ -271,7 +265,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioRecorder() { - let isRecording = audioRecorder?.isRecording ?? false + let isRecording = mediaServiceProvider.audioRecorder.isRecording displayLink.isPaused = !isRecording @@ -281,11 +275,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, padSamplesArrayToSize(requiredNumberOfSamples) } - let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0 + let sample = mediaServiceProvider.audioRecorder.averagePowerForChannelNumber(0) audioSamples.insert(sample, at: 0) audioSamples.removeLast() - let currentTime = audioRecorder?.currentTime ?? 0.0 + let currentTime = mediaServiceProvider.audioRecorder.currentTime if currentTime >= Constants.maximumAudioRecordingDuration { finishRecording() @@ -317,16 +311,12 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioPlayer() { - guard let audioPlayer = audioPlayer else { - return - } - - guard let url = audioPlayer.url else { + guard let url = mediaServiceProvider.audioPlayer.url else { MXLog.error("Invalid audio player url.") return } - displayLink.isPaused = !audioPlayer.isPlaying + displayLink.isPaused = !mediaServiceProvider.audioPlayer.isPlaying let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() if audioSamples.count != requiredNumberOfSamples && requiredNumberOfSamples > 0 { @@ -347,11 +337,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } var details = VoiceMessageToolbarViewDetails() - details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) + details.state = (mediaServiceProvider.audioRecorder.isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (mediaServiceProvider.audioPlayer.isPlaying ? mediaServiceProvider.audioPlayer.currentTime : mediaServiceProvider.audioPlayer.duration))) details.audioSamples = audioSamples - details.isPlaying = audioPlayer.isPlaying - details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + details.isPlaying = mediaServiceProvider.audioPlayer.isPlaying + details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) _voiceMessageToolbarView.configureWithDetails(details) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index f6b558af85..7d455c0a3d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -18,63 +18,32 @@ import Foundation @objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate { - private let audioPlayers: NSHashTable - private let audioRecorders: NSHashTable - + let audioPlayer = VoiceMessageAudioPlayer() + var mediaIdentifier: String? + let audioRecorder = VoiceMessageAudioRecorder() + @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() private override init() { - audioPlayers = NSHashTable(options: .weakMemory) - audioRecorders = NSHashTable(options: .weakMemory) - } - - @objc func audioPlayer() -> VoiceMessageAudioPlayer { - let audioPlayer = VoiceMessageAudioPlayer() + super.init() audioPlayer.registerDelegate(self) - audioPlayers.add(audioPlayer) - return audioPlayer - } - - @objc func audioRecorder() -> VoiceMessageAudioRecorder { - let audioRecorder = VoiceMessageAudioRecorder() audioRecorder.registerDelegate(self) - audioRecorders.add(audioRecorder) - return audioRecorder } @objc func stopAllServices() { - stopAllServicesExcept(nil) + audioPlayer.stop() + audioRecorder.stopRecording() } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { - stopAllServicesExcept(audioPlayer) + audioRecorder.stopRecording() } // MARK: - VoiceMessageAudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - stopAllServicesExcept(audioRecorder) - } - - // MARK: - Private - - private func stopAllServicesExcept(_ service: AnyObject?) { - for audioPlayer in audioPlayers.allObjects { - if audioPlayer === service { - continue - } - - audioPlayer.pause() - } - - for audioRecoder in audioRecorders.allObjects { - if audioRecoder === service { - continue - } - - audioRecoder.stopRecording() - } + audioPlayer.stop() } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 443dfa7f7c..57aa4bba8a 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -37,8 +37,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess }() private let cacheManager: VoiceMessageAttachmentCacheManager - - private let audioPlayer: VoiceMessageAudioPlayer + private let mediaServiceProvider: VoiceMessageMediaServiceProvider private var displayLink: CADisplayLink! private var samples: [Float] = [] private var duration: TimeInterval = 0 @@ -47,6 +46,9 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess private var state: VoiceMessagePlaybackControllerState = .stopped { didSet { + if state == .stopped || state == .error { + mediaServiceProvider.audioPlayer.deregisterDelegate(self) + } updateUI() displayLink.isPaused = (state != .playing) } @@ -57,11 +59,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess init(mediaServiceProvider: VoiceMessageMediaServiceProvider, cacheManager: VoiceMessageAttachmentCacheManager) { self.cacheManager = cacheManager + self.mediaServiceProvider = mediaServiceProvider playbackView = VoiceMessagePlaybackView.loadFromNib() - audioPlayer = mediaServiceProvider.audioPlayer() - audioPlayer.registerDelegate(self) playbackView.delegate = self displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -82,16 +83,29 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess // MARK: - VoiceMessagePlaybackViewDelegate func voiceMessagePlaybackViewDidRequestPlaybackToggle() { - if audioPlayer.isPlaying { - audioPlayer.pause() + if mediaServiceProvider.mediaIdentifier == attachment?.eventId { + if mediaServiceProvider.audioPlayer.isPlaying { + mediaServiceProvider.audioPlayer.pause() + } else { + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.play() + } } else { - audioPlayer.play() + if let url = urlToLoad { + mediaServiceProvider.mediaIdentifier = attachment?.eventId + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.loadContentFromURL(url) + mediaServiceProvider.audioPlayer.play() + } } } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { + if audioPlayer.url != self.urlToLoad { + state = .stopped + } updateUI() } @@ -135,8 +149,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) - details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: mediaServiceProvider.audioPlayer.currentTime)) + details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) } details.loading = self.loading @@ -148,6 +162,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return } + mediaServiceProvider.audioPlayer.deregisterDelegate(self) self.state = .stopped self.loading = true self.samples = [] @@ -163,10 +178,21 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } self.loading = false - self.audioPlayer.loadContentFromURL(result.1) + self.urlToLoad = result.1 self.duration = result.2 self.samples = result.3 + if self.mediaServiceProvider.mediaIdentifier == self.attachment?.eventId { + self.mediaServiceProvider.audioPlayer.registerDelegate(self) + if self.mediaServiceProvider.audioPlayer.isPlaying { + self.state = .playing + } else if self.mediaServiceProvider.audioPlayer.currentTime > 0 { + self.state = .paused + } else { + self.state = .stopped + } + } + self.updateUI() case .failure: self.state = .error From 227197375c276193fd46d2c028b3b32ab83e2a06 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 13 Jul 2021 16:03:20 +0300 Subject: [PATCH 060/125] #4094 - Sending voice message recording length and waveform samples. --- Config/BuildSettings.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 8 ++- .../VoiceMessageController.swift | 65 ++++++++++++++++--- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 9ffad81286..30e86e015c 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -311,7 +311,7 @@ final class BuildSettings: NSObject { // MARK: - Voice Message - static let voiceMessagesEnabled = false + static let voiceMessagesEnabled = true // MARK: - HTTP /// Additional HTTP headers will be sent by all requests. Not recommended to use request-specific headers, like `Authorization`. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7a6663faf1..28e6e04dc1 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6188,9 +6188,13 @@ - (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageContro }]; } -- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController didRequestSendForFileAtURL:(NSURL *)url completion:(void (^)(BOOL))completion +- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController + didRequestSendForFileAtURL:(NSURL *)url + duration:(NSTimeInterval)duration + samples:(NSArray *)samples + completion:(void (^)(BOOL))completion { - [self.roomDataSource sendVoiceMessage:url mimeType:nil success:^(NSString *eventId) { + [self.roomDataSource sendVoiceMessage:url mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { MXLogDebug(@"Success with event id %@", eventId); completion(YES); } failure:^(NSError *error) { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index dd641929bc..d24be6379c 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -20,7 +20,7 @@ import DSWaveformImage @objc public protocol VoiceMessageControllerDelegate: AnyObject { func voiceMessageControllerDidRequestMicrophonePermission(_ voiceMessageController: VoiceMessageController) - func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, completion: @escaping (Bool) -> Void) + func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, duration: TimeInterval, samples: [Float]?, completion: @escaping (Bool) -> Void) } public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate, VoiceMessageAudioPlayerDelegate { @@ -215,21 +215,68 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private func sendRecordingAtURL(_ sourceURL: URL) { - let destinationURL = sourceURL.deletingPathExtension().appendingPathExtension("opus") + let dispatchGroup = DispatchGroup() - VoiceMessageAudioConverter.convertToOpusOgg(sourceURL: sourceURL, destinationURL: destinationURL) { [weak self] result in - guard let self = self else { return } - + var duration = 0.0 + var invertedSamples: [Float]? + var finalURL: URL? + + dispatchGroup.enter() + VoiceMessageAudioConverter.mediaDurationAt(sourceURL) { result in switch result { case .success: - self.delegate?.voiceMessageController(self, didRequestSendForFileAtURL: destinationURL) { [weak self] success in - UINotificationFeedbackGenerator().notificationOccurred((success ? .success : .error)) - self?.deleteRecordingAtURL(sourceURL) - self?.deleteRecordingAtURL(destinationURL) + if let someDuration = try? result.get() { + duration = someDuration + } else { + MXLog.error("[VoiceMessageController] Failed retrieving media duration") } + case .failure(let error): + MXLog.error("[VoiceMessageController] Failed getting audio duration with: \(error)") + } + + dispatchGroup.leave() + } + + dispatchGroup.enter() + let analyser = WaveformAnalyzer(audioAssetURL: sourceURL) + analyser?.samples(count: 100, completionHandler: { samples in + // Dispatch back from the WaveformAnalyzer's internal queue + DispatchQueue.main.async { + if let samples = samples { + invertedSamples = samples.compactMap { return 1.0 - $0 } // linearly normalized to [0, 1] (1 -> -50 dB) + } else { + MXLog.error("[VoiceMessageController] Failed sampling recorder voice message.") + } + + dispatchGroup.leave() + } + }) + + dispatchGroup.enter() + let destinationURL = sourceURL.deletingPathExtension().appendingPathExtension("opus") + VoiceMessageAudioConverter.convertToOpusOgg(sourceURL: sourceURL, destinationURL: destinationURL) { result in + switch result { + case .success: + finalURL = destinationURL case .failure(let error): MXLog.error("Failed failed encoding audio message with: \(error)") } + + dispatchGroup.leave() + } + + dispatchGroup.notify(queue: .main) { + guard let url = finalURL else { + return + } + + self.delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url, + duration: duration, + samples: invertedSamples) { [weak self] success in + UINotificationFeedbackGenerator().notificationOccurred((success ? .success : .error)) + self?.deleteRecordingAtURL(sourceURL) + self?.deleteRecordingAtURL(destinationURL) + } } } From ff9384c079f53944416c3288d1c6f9a9ec2e5656 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 13 Jul 2021 16:13:39 +0300 Subject: [PATCH 061/125] Revert "Revert "#4545 - Switch back to using multiple audio player instances, allow pausing when starting a new player."" This reverts commit 1a5197c69a9caa0e961fd58939a76e4c4349468e. --- .../VoiceMessageController.swift | 64 +++++++++++-------- .../VoiceMessageMediaServiceProvider.swift | 49 +++++++++++--- .../VoiceMessagePlaybackController.swift | 46 +++---------- 3 files changed, 87 insertions(+), 72 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index d24be6379c..c044c96c2d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -44,6 +44,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private let _voiceMessageToolbarView: VoiceMessageToolbarView private var displayLink: CADisplayLink! + private var audioRecorder: VoiceMessageAudioRecorder? + + private var audioPlayer: VoiceMessageAudioPlayer? private var waveformAnalyser: WaveformAnalyzer? private var audioSamples: [Float] = [] @@ -53,7 +56,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, @objc public weak var delegate: VoiceMessageControllerDelegate? @objc public var isRecordingAudio: Bool { - return mediaServiceProvider.audioRecorder.isRecording || isInLockedMode + return audioRecorder?.isRecording ?? false || isInLockedMode } @objc public var voiceMessageToolbarView: UIView { @@ -101,8 +104,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") - mediaServiceProvider.audioRecorder.registerDelegate(self) - mediaServiceProvider.audioRecorder.recordWithOuputURL(temporaryFileURL) + audioRecorder = mediaServiceProvider.audioRecorder() + audioRecorder?.registerDelegate(self) + audioRecorder?.recordWithOuputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -111,8 +115,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { isInLockedMode = false - mediaServiceProvider.audioRecorder.stopRecording() - deleteRecordingAtURL(mediaServiceProvider.audioRecorder.url) + audioRecorder?.stopRecording() + deleteRecordingAtURL(audioRecorder?.url) UINotificationFeedbackGenerator().notificationOccurred(.error) updateUI() } @@ -123,21 +127,21 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { - if mediaServiceProvider.audioPlayer.isPlaying { - mediaServiceProvider.audioPlayer.pause() + if audioPlayer?.isPlaying ?? false { + audioPlayer?.pause() } else { - mediaServiceProvider.audioPlayer.play() + audioPlayer?.play() } } func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) { - guard let url = mediaServiceProvider.audioRecorder.url else { + guard let url = audioRecorder?.url else { MXLog.error("Invalid audio recording URL") return } - mediaServiceProvider.audioPlayer.stop() - mediaServiceProvider.audioRecorder.stopRecording() + audioPlayer?.stop() + audioRecorder?.stopRecording() sendRecordingAtURL(url) @@ -191,23 +195,25 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - Private private func finishRecording() { - let recordDuration = mediaServiceProvider.audioRecorder.currentTime - mediaServiceProvider.audioRecorder.stopRecording() + let recordDuration = audioRecorder?.currentTime + audioRecorder?.stopRecording() - guard let url = mediaServiceProvider.audioRecorder.url else { + guard let url = audioRecorder?.url else { MXLog.error("Invalid audio recording URL") return } guard isInLockedMode else { - if recordDuration >= Constants.minimumRecordingDuration { + if recordDuration ?? 0 >= Constants.minimumRecordingDuration { sendRecordingAtURL(url) } return } - mediaServiceProvider.audioPlayer.registerDelegate(self) - mediaServiceProvider.audioPlayer.loadContentFromURL(url) + audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer?.registerDelegate(self) + audioPlayer?.loadContentFromURL(url) + audioSamples = [] updateUI() @@ -302,7 +308,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private func updateUI() { - let shouldUpdateFromAudioPlayer = isInLockedMode && !mediaServiceProvider.audioRecorder.isRecording + let shouldUpdateFromAudioPlayer = isInLockedMode && !(audioRecorder?.isRecording ?? false) if shouldUpdateFromAudioPlayer { updateUIFromAudioPlayer() @@ -312,7 +318,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioRecorder() { - let isRecording = mediaServiceProvider.audioRecorder.isRecording + let isRecording = audioRecorder?.isRecording ?? false displayLink.isPaused = !isRecording @@ -322,11 +328,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, padSamplesArrayToSize(requiredNumberOfSamples) } - let sample = mediaServiceProvider.audioRecorder.averagePowerForChannelNumber(0) + let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0 audioSamples.insert(sample, at: 0) audioSamples.removeLast() - let currentTime = mediaServiceProvider.audioRecorder.currentTime + let currentTime = audioRecorder?.currentTime ?? 0.0 if currentTime >= Constants.maximumAudioRecordingDuration { finishRecording() @@ -358,12 +364,16 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } private func updateUIFromAudioPlayer() { - guard let url = mediaServiceProvider.audioPlayer.url else { + guard let audioPlayer = audioPlayer else { + return + } + + guard let url = audioPlayer.url else { MXLog.error("Invalid audio player url.") return } - displayLink.isPaused = !mediaServiceProvider.audioPlayer.isPlaying + displayLink.isPaused = !audioPlayer.isPlaying let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() if audioSamples.count != requiredNumberOfSamples && requiredNumberOfSamples > 0 { @@ -384,11 +394,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } var details = VoiceMessageToolbarViewDetails() - details.state = (mediaServiceProvider.audioRecorder.isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (mediaServiceProvider.audioPlayer.isPlaying ? mediaServiceProvider.audioPlayer.currentTime : mediaServiceProvider.audioPlayer.duration))) + details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) details.audioSamples = audioSamples - details.isPlaying = mediaServiceProvider.audioPlayer.isPlaying - details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) + details.isPlaying = audioPlayer.isPlaying + details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) _voiceMessageToolbarView.configureWithDetails(details) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 7d455c0a3d..f6b558af85 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -18,32 +18,63 @@ import Foundation @objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate { - let audioPlayer = VoiceMessageAudioPlayer() - var mediaIdentifier: String? - let audioRecorder = VoiceMessageAudioRecorder() - + private let audioPlayers: NSHashTable + private let audioRecorders: NSHashTable + @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() private override init() { - super.init() + audioPlayers = NSHashTable(options: .weakMemory) + audioRecorders = NSHashTable(options: .weakMemory) + } + + @objc func audioPlayer() -> VoiceMessageAudioPlayer { + let audioPlayer = VoiceMessageAudioPlayer() audioPlayer.registerDelegate(self) + audioPlayers.add(audioPlayer) + return audioPlayer + } + + @objc func audioRecorder() -> VoiceMessageAudioRecorder { + let audioRecorder = VoiceMessageAudioRecorder() audioRecorder.registerDelegate(self) + audioRecorders.add(audioRecorder) + return audioRecorder } @objc func stopAllServices() { - audioPlayer.stop() - audioRecorder.stopRecording() + stopAllServicesExcept(nil) } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { - audioRecorder.stopRecording() + stopAllServicesExcept(audioPlayer) } // MARK: - VoiceMessageAudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - audioPlayer.stop() + stopAllServicesExcept(audioRecorder) + } + + // MARK: - Private + + private func stopAllServicesExcept(_ service: AnyObject?) { + for audioPlayer in audioPlayers.allObjects { + if audioPlayer === service { + continue + } + + audioPlayer.pause() + } + + for audioRecoder in audioRecorders.allObjects { + if audioRecoder === service { + continue + } + + audioRecoder.stopRecording() + } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 57aa4bba8a..443dfa7f7c 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -37,7 +37,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess }() private let cacheManager: VoiceMessageAttachmentCacheManager - private let mediaServiceProvider: VoiceMessageMediaServiceProvider + + private let audioPlayer: VoiceMessageAudioPlayer private var displayLink: CADisplayLink! private var samples: [Float] = [] private var duration: TimeInterval = 0 @@ -46,9 +47,6 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess private var state: VoiceMessagePlaybackControllerState = .stopped { didSet { - if state == .stopped || state == .error { - mediaServiceProvider.audioPlayer.deregisterDelegate(self) - } updateUI() displayLink.isPaused = (state != .playing) } @@ -59,10 +57,11 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess init(mediaServiceProvider: VoiceMessageMediaServiceProvider, cacheManager: VoiceMessageAttachmentCacheManager) { self.cacheManager = cacheManager - self.mediaServiceProvider = mediaServiceProvider playbackView = VoiceMessagePlaybackView.loadFromNib() + audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer.registerDelegate(self) playbackView.delegate = self displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -83,29 +82,16 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess // MARK: - VoiceMessagePlaybackViewDelegate func voiceMessagePlaybackViewDidRequestPlaybackToggle() { - if mediaServiceProvider.mediaIdentifier == attachment?.eventId { - if mediaServiceProvider.audioPlayer.isPlaying { - mediaServiceProvider.audioPlayer.pause() - } else { - mediaServiceProvider.audioPlayer.registerDelegate(self) - mediaServiceProvider.audioPlayer.play() - } + if audioPlayer.isPlaying { + audioPlayer.pause() } else { - if let url = urlToLoad { - mediaServiceProvider.mediaIdentifier = attachment?.eventId - mediaServiceProvider.audioPlayer.registerDelegate(self) - mediaServiceProvider.audioPlayer.loadContentFromURL(url) - mediaServiceProvider.audioPlayer.play() - } + audioPlayer.play() } } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { - if audioPlayer.url != self.urlToLoad { - state = .stopped - } updateUI() } @@ -149,8 +135,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: mediaServiceProvider.audioPlayer.currentTime)) - details.progress = (mediaServiceProvider.audioPlayer.duration > 0.0 ? mediaServiceProvider.audioPlayer.currentTime / mediaServiceProvider.audioPlayer.duration : 0.0) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) + details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) } details.loading = self.loading @@ -162,7 +148,6 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return } - mediaServiceProvider.audioPlayer.deregisterDelegate(self) self.state = .stopped self.loading = true self.samples = [] @@ -178,21 +163,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } self.loading = false - self.urlToLoad = result.1 + self.audioPlayer.loadContentFromURL(result.1) self.duration = result.2 self.samples = result.3 - if self.mediaServiceProvider.mediaIdentifier == self.attachment?.eventId { - self.mediaServiceProvider.audioPlayer.registerDelegate(self) - if self.mediaServiceProvider.audioPlayer.isPlaying { - self.state = .playing - } else if self.mediaServiceProvider.audioPlayer.currentTime > 0 { - self.state = .paused - } else { - self.state = .stopped - } - } - self.updateUI() case .failure: self.state = .error From 300379b765afaf72ae6e8348faaf419124fe02db Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 13 Jul 2021 17:55:42 +0100 Subject: [PATCH 062/125] Update addressable gem. --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 31e7491fee..3548311eed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) @@ -252,4 +252,4 @@ DEPENDENCIES xcode-install BUNDLED WITH - 2.2.14 + 2.2.21 From 1181ccafff6350505e63caa443392d83d6851ac0 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 14 Jul 2021 10:26:21 +0300 Subject: [PATCH 063/125] #4094 - Switched back to multiple audio player instances (1 per event identifier), strongly retaining the currenty playing one and unloading all stopped ones. Various other improvements and bug fixes. --- .../VoiceMessageAttachmentCacheManager.swift | 23 ++++++-- .../VoiceMessageAudioPlayer.swift | 5 ++ .../VoiceMessageController.swift | 28 ++++----- .../VoiceMessageMediaServiceProvider.swift | 39 +++++++++---- .../VoiceMessagePlaybackController.swift | 58 ++++++++++++++----- 5 files changed, 108 insertions(+), 45 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 7890fd3a03..127797926f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -31,13 +31,20 @@ enum VoiceMessageAttachmentCacheManagerError: Error { Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array. */ private class CompletionWrapper { - let completion: (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void + let completion: (Result) -> Void - init(_ completion: @escaping (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void) { + init(_ completion: @escaping (Result) -> Void) { self.completion = completion } } +struct VoiceMessageAttachmentCacheManagerLoadResult { + let eventIdentifier: String + let url: URL + let duration: TimeInterval + let samples: [Float] +} + class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() @@ -48,9 +55,10 @@ class VoiceMessageAttachmentCacheManager { private var finalURLs = [String: URL]() private init() { + } - func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(String, URL, TimeInterval, [Float]), Error>) -> Void) { + func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result) -> Void) { guard attachment.type == MXKAttachmentTypeVoiceMessage else { completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) return @@ -67,14 +75,15 @@ class VoiceMessageAttachmentCacheManager { } if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] { - completion(Result.success((identifier, finalURL, duration, samples))) + let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) + completion(Result.success(result)) return } self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } - private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(String, URL, Double, [Float]), Error>) -> Void) { + private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result) -> Void) { if var callbacks = completionCallbacks[identifier] { callbacks.append(CompletionWrapper(completion)) @@ -170,10 +179,12 @@ class VoiceMessageAttachmentCacheManager { return } + let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: url, duration: duration, samples: samples) + let copy = callbacks.map { $0 } DispatchQueue.main.async { for wrapper in copy { - wrapper.completion(Result.success((identifier, url, duration, samples))) + wrapper.completion(Result.success(result)) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 76f5dfbfcc..4da9f97791 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -99,6 +99,11 @@ class VoiceMessageAudioPlayer: NSObject { addObservers() } + func unloadContent() { + url = nil + audioPlayer?.replaceCurrentItem(with: nil) + } + func play() { isStopped = false diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index c044c96c2d..504d96da76 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -40,6 +40,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private let themeService: ThemeService private let mediaServiceProvider: VoiceMessageMediaServiceProvider + private let temporaryFileURL: URL private let _voiceMessageToolbarView: VoiceMessageToolbarView private var displayLink: CADisplayLink! @@ -67,6 +68,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, self.themeService = themeService self.mediaServiceProvider = mediaServiceProvider + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") + _voiceMessageToolbarView = VoiceMessageToolbarView.loadFromNib() super.init() @@ -100,9 +104,6 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } UIImpactFeedbackGenerator(style: .medium).impactOccurred() - - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") audioRecorder = mediaServiceProvider.audioRecorder() audioRecorder?.registerDelegate(self) @@ -127,9 +128,14 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { - if audioPlayer?.isPlaying ?? false { - audioPlayer?.pause() + if audioPlayer?.url != nil { + if audioPlayer?.isPlaying ?? false { + audioPlayer?.pause() + } else { + audioPlayer?.play() + } } else { + audioPlayer?.loadContentFromURL(temporaryFileURL) audioPlayer?.play() } } @@ -210,9 +216,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, return } - audioPlayer = mediaServiceProvider.audioPlayer() + audioPlayer = mediaServiceProvider.audioPlayerForIdentifier(UUID().uuidString) audioPlayer?.registerDelegate(self) - audioPlayer?.loadContentFromURL(url) audioSamples = [] @@ -368,18 +373,13 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, return } - guard let url = audioPlayer.url else { - MXLog.error("Invalid audio player url.") - return - } - displayLink.isPaused = !audioPlayer.isPlaying let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() if audioSamples.count != requiredNumberOfSamples && requiredNumberOfSamples > 0 { padSamplesArrayToSize(requiredNumberOfSamples) - waveformAnalyser = WaveformAnalyzer(audioAssetURL: url) + waveformAnalyser = WaveformAnalyzer(audioAssetURL: temporaryFileURL) waveformAnalyser?.samples(count: requiredNumberOfSamples, completionHandler: { [weak self] samples in guard let samples = samples else { MXLog.error("Could not sample audio recording.") @@ -398,7 +398,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) details.audioSamples = audioSamples details.isPlaying = audioPlayer.isPlaying - details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + details.progress = (audioPlayer.isPlaying ? (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) : 0.0) _voiceMessageToolbarView.configureWithDetails(details) } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index f6b558af85..82a16c977c 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -18,20 +18,27 @@ import Foundation @objc public class VoiceMessageMediaServiceProvider: NSObject, VoiceMessageAudioPlayerDelegate, VoiceMessageAudioRecorderDelegate { - private let audioPlayers: NSHashTable + private let audioPlayers: NSMapTable private let audioRecorders: NSHashTable + // Retain currently playing audio player so it doesn't stop playing on timeline cell reusage + private var currentlyPlayingAudioPlayer: VoiceMessageAudioPlayer? + @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() private override init() { - audioPlayers = NSHashTable(options: .weakMemory) + audioPlayers = NSMapTable(valueOptions: .weakMemory) audioRecorders = NSHashTable(options: .weakMemory) } - @objc func audioPlayer() -> VoiceMessageAudioPlayer { + @objc func audioPlayerForIdentifier(_ identifier: String) -> VoiceMessageAudioPlayer { + if let audioPlayer = audioPlayers.object(forKey: identifier as NSString) { + return audioPlayer + } + let audioPlayer = VoiceMessageAudioPlayer() audioPlayer.registerDelegate(self) - audioPlayers.add(audioPlayer) + audioPlayers.setObject(audioPlayer, forKey: identifier as NSString) return audioPlayer } @@ -49,9 +56,16 @@ import Foundation // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + currentlyPlayingAudioPlayer = audioPlayer stopAllServicesExcept(audioPlayer) } + func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + if currentlyPlayingAudioPlayer == audioPlayer { + currentlyPlayingAudioPlayer = nil + } + } + // MARK: - VoiceMessageAudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { @@ -61,20 +75,25 @@ import Foundation // MARK: - Private private func stopAllServicesExcept(_ service: AnyObject?) { - for audioPlayer in audioPlayers.allObjects { - if audioPlayer === service { + for audioRecoder in audioRecorders.allObjects { + if audioRecoder === service { continue } - audioPlayer.pause() + audioRecoder.stopRecording() } - for audioRecoder in audioRecorders.allObjects { - if audioRecoder === service { + guard let audioPlayersEnumerator = audioPlayers.objectEnumerator() else { + return + } + + for case let audioPlayer as VoiceMessageAudioPlayer in audioPlayersEnumerator { + if audioPlayer === service { continue } - audioRecoder.stopRecording() + audioPlayer.stop() + audioPlayer.unloadContent() } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 443dfa7f7c..d3fc05e74e 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -36,9 +36,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess return dateFormatter }() + private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let cacheManager: VoiceMessageAttachmentCacheManager - private let audioPlayer: VoiceMessageAudioPlayer + private var audioPlayer: VoiceMessageAudioPlayer? private var displayLink: CADisplayLink! private var samples: [Float] = [] private var duration: TimeInterval = 0 @@ -56,12 +57,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess init(mediaServiceProvider: VoiceMessageMediaServiceProvider, cacheManager: VoiceMessageAttachmentCacheManager) { + self.mediaServiceProvider = mediaServiceProvider self.cacheManager = cacheManager playbackView = VoiceMessagePlaybackView.loadFromNib() - audioPlayer = mediaServiceProvider.audioPlayer() - - audioPlayer.registerDelegate(self) playbackView.delegate = self displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -82,9 +81,18 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess // MARK: - VoiceMessagePlaybackViewDelegate func voiceMessagePlaybackViewDidRequestPlaybackToggle() { - if audioPlayer.isPlaying { - audioPlayer.pause() - } else { + guard let audioPlayer = audioPlayer else { + return + } + + if audioPlayer.url != nil { + if audioPlayer.isPlaying { + audioPlayer.pause() + } else { + audioPlayer.play() + } + } else if let url = urlToLoad { + audioPlayer.loadContentFromURL(url) audioPlayer.play() } } @@ -135,8 +143,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) - details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + if let audioPlayer = audioPlayer { + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) + details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) + } } details.loading = self.loading @@ -155,19 +165,37 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() - cacheManager.loadAttachment(attachment, numberOfSamples: requiredNumberOfSamples) { result in + cacheManager.loadAttachment(attachment, numberOfSamples: requiredNumberOfSamples) { [weak self] result in + guard let self = self else { + return + } + switch result { case .success(let result): - guard result.0 == attachment.eventId else { + guard result.eventIdentifier == attachment.eventId else { return } + // Avoid listening to old audio player delegates if the attachment for this playbackController/cell changes + self.audioPlayer?.deregisterDelegate(self) + + self.audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) + self.audioPlayer?.registerDelegate(self) + self.loading = false - self.audioPlayer.loadContentFromURL(result.1) - self.duration = result.2 - self.samples = result.3 + self.urlToLoad = result.url + self.duration = result.duration + self.samples = result.samples - self.updateUI() + if let audioPlayer = self.audioPlayer { + if audioPlayer.isPlaying { + self.state = .playing + } else if audioPlayer.currentTime > 0 { + self.state = .paused + } else { + self.state = .stopped + } + } case .failure: self.state = .error } From cb04c97e47569f95b2b10d42678cf5aa368640f9 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Wed, 14 Jul 2021 10:54:19 +0200 Subject: [PATCH 064/125] Add bug report information --- .github/ISSUE_TEMPLATE/bug_report.md | 31 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yaml | 8 ++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..c6b2295390 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +#### Describe the bug. +A clear and concise description of what the bug is. + +#### Steps to reproduce: +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +#### Expected behavior +A clear and concise description of what you expected to happen. + +#### Screenshots +If applicable, add screenshots to help explain your problem. + +#### Contextual information: + + - Device: + - OS: + + - App Version: \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000000..b30282798d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Element iOS Community Support + url: "https://matrix.to/#/#element-ios:matrix.org" + about: General Element iOS support questions can be asked here. + - name: Matrix Security Policy + url: https://www.matrix.org/security-disclosure-policy/ + about: Learn more about our security disclosure policy. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..5a372c1468 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'feature' +assignees: '' + +--- + +#### Is your feature request related to a problem? Please describe. +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +#### Describe the solution you'd like. +A clear and concise description of what you want to happen. + +#### Describe alternatives you've considered. +A clear and concise description of any alternative solutions or features you've considered. + +#### Additional context. +Add any other context or screenshots about the feature request here. \ No newline at end of file From 629004dcfaba7384c3988a106ffa6c45a4f35906 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Wed, 14 Jul 2021 11:01:55 +0200 Subject: [PATCH 065/125] add to changes.rst --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index bc28f2c309..66a9ca1c79 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,6 +29,7 @@ Changes to be released in next version Others * Silenced some documentation, deprecations and SwiftLint warnings. + * Updated issue templates. Changes in 1.4.4 (2021-06-30) ================================================= From a3ec3356db6fb7589a9ec4c33834ae221afe3ea4 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 14 Jul 2021 14:40:54 +0300 Subject: [PATCH 066/125] #4094 - Fixed flickering elapsed time labels and other tweaks. --- .../VoiceMessageAudioPlayer.swift | 4 ++ .../VoiceMessageController.swift | 59 ++++++++++--------- .../VoiceMessagePlaybackController.swift | 28 +++++---- .../VoiceMessagePlaybackView.swift | 2 +- 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 4da9f97791..1fe9283021 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -121,6 +121,10 @@ class VoiceMessageAudioPlayer: NSObject { } func stop() { + if isStopped { + return + } + isStopped = true audioPlayer?.pause() audioPlayer?.seek(to: .zero) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 504d96da76..d6e330fa44 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -28,16 +28,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private enum Constants { static let maximumAudioRecordingDuration: TimeInterval = 120.0 static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0 - static let elapsedTimeFormat = "m:ss" static let minimumRecordingDuration = 1.0 } - private static let timeFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = Constants.elapsedTimeFormat - return dateFormatter - }() - private let themeService: ThemeService private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let temporaryFileURL: URL @@ -116,8 +109,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { isInLockedMode = false + audioPlayer?.stop() audioRecorder?.stopRecording() - deleteRecordingAtURL(audioRecorder?.url) + deleteRecordingAtURL(temporaryFileURL) UINotificationFeedbackGenerator().notificationOccurred(.error) updateUI() } @@ -128,28 +122,27 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) { - if audioPlayer?.url != nil { - if audioPlayer?.isPlaying ?? false { - audioPlayer?.pause() + guard let audioPlayer = audioPlayer else { + return + } + + if audioPlayer.url != nil { + if audioPlayer.isPlaying { + audioPlayer.pause() } else { - audioPlayer?.play() + audioPlayer.play() } } else { - audioPlayer?.loadContentFromURL(temporaryFileURL) - audioPlayer?.play() + audioPlayer.loadContentFromURL(temporaryFileURL) + audioPlayer.play() } } func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) { - guard let url = audioRecorder?.url else { - MXLog.error("Invalid audio recording URL") - return - } - audioPlayer?.stop() audioRecorder?.stopRecording() - sendRecordingAtURL(url) + sendRecordingAtURL(temporaryFileURL) isInLockedMode = false updateUI() @@ -204,20 +197,16 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, let recordDuration = audioRecorder?.currentTime audioRecorder?.stopRecording() - guard let url = audioRecorder?.url else { - MXLog.error("Invalid audio recording URL") - return - } - guard isInLockedMode else { if recordDuration ?? 0 >= Constants.minimumRecordingDuration { - sendRecordingAtURL(url) + sendRecordingAtURL(temporaryFileURL) } return } audioPlayer = mediaServiceProvider.audioPlayerForIdentifier(UUID().uuidString) audioPlayer?.registerDelegate(self) + audioPlayer?.loadContentFromURL(temporaryFileURL) audioSamples = [] @@ -346,7 +335,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, var details = VoiceMessageToolbarViewDetails() details.state = (isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: currentTime)) + details.elapsedTime = durationStringFromTimeInterval(currentTime) details.audioSamples = audioSamples if isRecording { @@ -395,7 +384,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, var details = VoiceMessageToolbarViewDetails() details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) + details.elapsedTime = durationStringFromTimeInterval(audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration) details.audioSamples = audioSamples details.isPlaying = audioPlayer.isPlaying details.progress = (audioPlayer.isPlaying ? (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) : 0.0) @@ -410,4 +399,18 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioSamples = audioSamples + [Float](repeating: 0.0, count: delta) } + + private func durationStringFromTimeInterval(_ interval: TimeInterval) -> String { + guard interval.isFinite else { + return "" + } + + var timeInterval = abs(interval) + let hours = trunc(timeInterval / 3600.0) + timeInterval -= hours * 3600.0 + let minutes = trunc(timeInterval / 60.0) + timeInterval -= minutes * 60.0 + + return String(format: "%01.0f:%02.0f", minutes, timeInterval) + } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index d3fc05e74e..c2f2a18c21 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -26,16 +26,6 @@ enum VoiceMessagePlaybackControllerState { class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMessagePlaybackViewDelegate { - private enum Constants { - static let elapsedTimeFormat = "m:ss" - } - - private static let timeFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = Constants.elapsedTimeFormat - return dateFormatter - }() - private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let cacheManager: VoiceMessageAttachmentCacheManager @@ -140,11 +130,11 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess switch state { case .stopped: - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) + details.currentTime = durationStringFromTimeInterval(self.duration) details.progress = 0.0 default: if let audioPlayer = audioPlayer { - details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) + details.currentTime = durationStringFromTimeInterval(audioPlayer.currentTime) details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) } } @@ -205,4 +195,18 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess @objc private func updateTheme() { playbackView.update(theme: ThemeService.shared().theme) } + + private func durationStringFromTimeInterval(_ interval: TimeInterval) -> String { + guard interval.isFinite else { + return "" + } + + var timeInterval = abs(interval) + let hours = trunc(timeInterval / 3600.0) + timeInterval -= hours * 3600.0 + let minutes = trunc(timeInterval / 60.0) + timeInterval -= minutes * 60.0 + + return String(format: "%01.0f:%02.0f", minutes, timeInterval) + } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index 36b2e5248e..4492b671fc 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -93,7 +93,7 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { elapsedTimeLabel.text = details.currentTime _waveformView.progress = details.progress _waveformView.samples = details.samples - _waveformView.alpha = 1 + _waveformView.alpha = 1.0 } self.details = details From aa264048ddec29ee4a0b91e18bd0d9bfdb4503fe Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Wed, 14 Jul 2021 17:00:21 +0100 Subject: [PATCH 067/125] Update and rename ci.yml to ci-build.yml Separate CI jobs into individual actions. --- .github/workflows/{ci.yml => ci-build.yml} | 42 +--------------------- 1 file changed, 1 insertion(+), 41 deletions(-) rename .github/workflows/{ci.yml => ci-build.yml} (56%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-build.yml similarity index 56% rename from .github/workflows/ci.yml rename to .github/workflows/ci-build.yml index 3e9e33f5c4..d1cd1b3a9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-build.yml @@ -1,4 +1,4 @@ -name: CI +name: Build CI on: # Triggers the workflow on any pull request and push to develop @@ -52,43 +52,3 @@ jobs: # Main step - name: Build iOS simulator run: bundle exec fastlane build - - - tests: - name: Tests - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - # Common cache - # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job - - uses: actions/cache@v2 - with: - path: Pods - key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - - uses: actions/cache@v2 - with: - path: vendor/bundle - key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gems- - - # Make sure we use the latest version of MatrixKit - - name: Reset MatrixKit pod - run: rm -rf Pods/MatrixKit - - # Common setup - # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job - - name: Bundle install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - name: Use right MatrixKit and MatrixSDK versions - run: bundle exec fastlane point_dependencies_to_related_branches - - # Main step - - name: Unit tests - run: bundle exec fastlane test From c41142920611cb2d33e2dd7dbad2d317064ab609 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Wed, 14 Jul 2021 17:01:15 +0100 Subject: [PATCH 068/125] Create ci-tests.yml Separate CI jobs into individual actions. --- .github/workflows/ci-tests.yml | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/ci-tests.yml diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 0000000000..eac0c885de --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,54 @@ +name: Tests CI + +on: + # Triggers the workflow on any pull request and push to develop + push: + branches: [ develop ] + pull_request: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + +jobs: + tests: + name: Tests + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + # Common cache + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + # Make sure we use the latest version of MatrixKit + - name: Reset MatrixKit pod + run: rm -rf Pods/MatrixKit + + # Common setup + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Use right MatrixKit and MatrixSDK versions + run: bundle exec fastlane point_dependencies_to_related_branches + + # Main step + - name: Unit tests + run: bundle exec fastlane test From 521c713d308a1939c925b0544b83d92c702e20bd Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 09:06:36 +0300 Subject: [PATCH 069/125] #4094 - Redrawing waveforms on bound changes. --- .../VoiceMessageAttachmentCacheManager.swift | 1 + .../VoiceMessages/VoiceMessagePlaybackController.swift | 4 ++++ .../Room/VoiceMessages/VoiceMessagePlaybackView.swift | 9 +++++++++ .../Room/VoiceMessages/VoiceMessageToolbarView.swift | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 127797926f..ece5b84082 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -105,6 +105,7 @@ class VoiceMessageAttachmentCacheManager { if var existingSamples = self.samples[identifier] { existingSamples[numberOfSamples] = samples + self.samples[identifier] = existingSamples } else { self.samples[identifier] = [numberOfSamples: samples] } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index c2f2a18c21..9abc99e3b3 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -87,6 +87,10 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } } + func voiceMessagePlaybackViewDidChangeWidth() { + loadAttachmentData() + } + // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index 4492b671fc..e3f53a4347 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -19,6 +19,7 @@ import Reusable protocol VoiceMessagePlaybackViewDelegate: AnyObject { func voiceMessagePlaybackViewDidRequestPlaybackToggle() + func voiceMessagePlaybackViewDidChangeWidth() } struct VoiceMessagePlaybackViewDetails { @@ -54,6 +55,14 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { return _waveformView } + override var bounds: CGRect { + didSet { + if oldValue.width != bounds.width { + delegate?.voiceMessagePlaybackViewDidChangeWidth() + } + } + } + override func awakeFromNib() { super.awakeFromNib() diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index d29dbeebb7..3cfaebff76 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -189,6 +189,10 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture delegate?.voiceMessageToolbarViewDidRequestPlaybackToggle(self) } + func voiceMessagePlaybackViewDidChangeWidth() { + + } + // MARK: - Private @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { From 9a3d26eae307820bd0fd5b21970f9a34007e64ef Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 12:05:47 +0300 Subject: [PATCH 070/125] #4094 - Reintroduced serial attachment loading processing queue and fixed completionCallback storage so they take the requestedNumberOfSamples into account. --- .../VoiceMessageAttachmentCacheManager.swift | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index ece5b84082..612de8a21d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -38,6 +38,11 @@ private class CompletionWrapper { } } +private struct CompletionCallbackKey: Hashable { + let eventIdentifier: String + let requiredNumberOfSamples: Int +} + struct VoiceMessageAttachmentCacheManagerLoadResult { let eventIdentifier: String let url: URL @@ -49,13 +54,15 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() - private var completionCallbacks = [String: [CompletionWrapper]]() + private var completionCallbacks = [CompletionCallbackKey: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var durations = [String: TimeInterval]() private var finalURLs = [String: URL]() + private let workQueue: DispatchQueue + private init() { - + workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result) -> Void) { @@ -74,30 +81,36 @@ class VoiceMessageAttachmentCacheManager { return } - if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] { - let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) - completion(Result.success(result)) - return + workQueue.async { + // Run this in the work queue to preserve order + if let finalURL = self.finalURLs[identifier], let duration = self.durations[identifier], let samples = self.samples[identifier]?[numberOfSamples] { + let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) + DispatchQueue.main.async { + completion(Result.success(result)) + } + return + } + + self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } - - self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result) -> Void) { - - if var callbacks = completionCallbacks[identifier] { + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: numberOfSamples) + + if var callbacks = completionCallbacks[callbackKey] { callbacks.append(CompletionWrapper(completion)) - completionCallbacks[identifier] = callbacks + completionCallbacks[callbackKey] = callbacks return } else { - completionCallbacks[identifier] = [CompletionWrapper(completion)] + completionCallbacks[callbackKey] = [CompletionWrapper(completion)] } func sampleFileAtURL(_ url: URL, duration: TimeInterval) { let analyser = WaveformAnalyzer(audioAssetURL: url) analyser?.samples(count: numberOfSamples, completionHandler: { samples in // Dispatch back from the WaveformAnalyzer's internal queue - DispatchQueue.main.async { + self.workQueue.async { guard let samples = samples else { self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) return @@ -176,7 +189,9 @@ class VoiceMessageAttachmentCacheManager { } private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) { - guard let callbacks = completionCallbacks[identifier] else { + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: samples.count) + + guard let callbacks = completionCallbacks[callbackKey] else { return } @@ -189,11 +204,13 @@ class VoiceMessageAttachmentCacheManager { } } - self.completionCallbacks[identifier] = nil + self.completionCallbacks[callbackKey] = nil } private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) { - guard let callbacks = completionCallbacks[identifier] else { + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: samples.count) + + guard let callbacks = completionCallbacks[callbackKey] else { return } @@ -204,6 +221,6 @@ class VoiceMessageAttachmentCacheManager { } } - self.completionCallbacks[identifier] = nil + self.completionCallbacks[callbackKey] = nil } } From 511569a76793538a388295994f68abc84c817d62 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 12:49:28 +0300 Subject: [PATCH 071/125] #4090 - Switched the sendVoiceMessage method duration parameter to an integer. --- Riot/Modules/Room/RoomViewController.m | 2 +- Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 28e6e04dc1..99c1981714 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6190,7 +6190,7 @@ - (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageContro - (void)voiceMessageController:(VoiceMessageController *)voiceMessageController didRequestSendForFileAtURL:(NSURL *)url - duration:(NSTimeInterval)duration + duration:(NSUInteger)duration samples:(NSArray *)samples completion:(void (^)(BOOL))completion { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index d6e330fa44..ea78372ffd 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -20,7 +20,7 @@ import DSWaveformImage @objc public protocol VoiceMessageControllerDelegate: AnyObject { func voiceMessageControllerDidRequestMicrophonePermission(_ voiceMessageController: VoiceMessageController) - func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, duration: TimeInterval, samples: [Float]?, completion: @escaping (Bool) -> Void) + func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, duration: UInt, samples: [Float]?, completion: @escaping (Bool) -> Void) } public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, VoiceMessageAudioRecorderDelegate, VoiceMessageAudioPlayerDelegate { @@ -271,7 +271,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } self.delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url, - duration: duration, + duration: UInt(duration * 1000), samples: invertedSamples) { [weak self] success in UINotificationFeedbackGenerator().notificationOccurred((success ? .success : .error)) self?.deleteRecordingAtURL(sourceURL) From cf884c6abf0e75f9d2b89a95012bd31d8200ae35 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 13:31:38 +0300 Subject: [PATCH 072/125] Revert "#4094 - Reintroduced serial attachment loading processing queue and fixed completionCallback storage so they take the requestedNumberOfSamples into account." This reverts commit 9a3d26eae307820bd0fd5b21970f9a34007e64ef. --- .../VoiceMessageAttachmentCacheManager.swift | 51 +++++++------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 612de8a21d..ece5b84082 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -38,11 +38,6 @@ private class CompletionWrapper { } } -private struct CompletionCallbackKey: Hashable { - let eventIdentifier: String - let requiredNumberOfSamples: Int -} - struct VoiceMessageAttachmentCacheManagerLoadResult { let eventIdentifier: String let url: URL @@ -54,15 +49,13 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() - private var completionCallbacks = [CompletionCallbackKey: [CompletionWrapper]]() + private var completionCallbacks = [String: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var durations = [String: TimeInterval]() private var finalURLs = [String: URL]() - private let workQueue: DispatchQueue - private init() { - workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) + } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result) -> Void) { @@ -81,36 +74,30 @@ class VoiceMessageAttachmentCacheManager { return } - workQueue.async { - // Run this in the work queue to preserve order - if let finalURL = self.finalURLs[identifier], let duration = self.durations[identifier], let samples = self.samples[identifier]?[numberOfSamples] { - let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) - DispatchQueue.main.async { - completion(Result.success(result)) - } - return - } - - self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) + if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] { + let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) + completion(Result.success(result)) + return } + + self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result) -> Void) { - let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: numberOfSamples) - - if var callbacks = completionCallbacks[callbackKey] { + + if var callbacks = completionCallbacks[identifier] { callbacks.append(CompletionWrapper(completion)) - completionCallbacks[callbackKey] = callbacks + completionCallbacks[identifier] = callbacks return } else { - completionCallbacks[callbackKey] = [CompletionWrapper(completion)] + completionCallbacks[identifier] = [CompletionWrapper(completion)] } func sampleFileAtURL(_ url: URL, duration: TimeInterval) { let analyser = WaveformAnalyzer(audioAssetURL: url) analyser?.samples(count: numberOfSamples, completionHandler: { samples in // Dispatch back from the WaveformAnalyzer's internal queue - self.workQueue.async { + DispatchQueue.main.async { guard let samples = samples else { self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) return @@ -189,9 +176,7 @@ class VoiceMessageAttachmentCacheManager { } private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) { - let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: samples.count) - - guard let callbacks = completionCallbacks[callbackKey] else { + guard let callbacks = completionCallbacks[identifier] else { return } @@ -204,13 +189,11 @@ class VoiceMessageAttachmentCacheManager { } } - self.completionCallbacks[callbackKey] = nil + self.completionCallbacks[identifier] = nil } private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) { - let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: samples.count) - - guard let callbacks = completionCallbacks[callbackKey] else { + guard let callbacks = completionCallbacks[identifier] else { return } @@ -221,6 +204,6 @@ class VoiceMessageAttachmentCacheManager { } } - self.completionCallbacks[callbackKey] = nil + self.completionCallbacks[identifier] = nil } } From 488338f7d9f5d636b9f12493d775a1b6b899463a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 13:32:19 +0300 Subject: [PATCH 073/125] #4090 - Changed FFMpeg-Kit to the LTS version and moved the project back to deployment target 11.0. --- Config/Project.xcconfig | 2 +- Podfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index c3c50db549..5772467eef 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -25,7 +25,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 12.1 +IPHONEOS_DEPLOYMENT_TARGET = 11.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 SWIFT_VERSION = 5.3.1 diff --git a/Podfile b/Podfile index 5d1e6b212c..4c799b26f7 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.1' +platform :ios, '11.0' # Use frameforks to allow usage of pod written in Swift (like PiwikTracker) use_frameworks! @@ -70,7 +70,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' - pod 'ffmpeg-kit-ios-audio', '~> 4.4' + pod 'ffmpeg-kit-ios-audio', '~> 4.4.LTS' pod 'FLEX', '~> 4.4.1', :configurations => ['Debug'] From 427e77e619ecb25562ece6ea4a6e23283e415af5 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 13:46:06 +0300 Subject: [PATCH 074/125] Revert "Revert "#4094 - Reintroduced serial attachment loading processing queue and fixed completionCallback storage so they take the requestedNumberOfSamples into account."" This reverts commit cf884c6abf0e75f9d2b89a95012bd31d8200ae35. --- .../VoiceMessageAttachmentCacheManager.swift | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index ece5b84082..612de8a21d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -38,6 +38,11 @@ private class CompletionWrapper { } } +private struct CompletionCallbackKey: Hashable { + let eventIdentifier: String + let requiredNumberOfSamples: Int +} + struct VoiceMessageAttachmentCacheManagerLoadResult { let eventIdentifier: String let url: URL @@ -49,13 +54,15 @@ class VoiceMessageAttachmentCacheManager { static let sharedManager = VoiceMessageAttachmentCacheManager() - private var completionCallbacks = [String: [CompletionWrapper]]() + private var completionCallbacks = [CompletionCallbackKey: [CompletionWrapper]]() private var samples = [String: [Int: [Float]]]() private var durations = [String: TimeInterval]() private var finalURLs = [String: URL]() + private let workQueue: DispatchQueue + private init() { - + workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated) } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result) -> Void) { @@ -74,30 +81,36 @@ class VoiceMessageAttachmentCacheManager { return } - if let finalURL = finalURLs[identifier], let duration = durations[identifier], let samples = samples[identifier]?[numberOfSamples] { - let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) - completion(Result.success(result)) - return + workQueue.async { + // Run this in the work queue to preserve order + if let finalURL = self.finalURLs[identifier], let duration = self.durations[identifier], let samples = self.samples[identifier]?[numberOfSamples] { + let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples) + DispatchQueue.main.async { + completion(Result.success(result)) + } + return + } + + self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } - - self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion) } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result) -> Void) { - - if var callbacks = completionCallbacks[identifier] { + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: numberOfSamples) + + if var callbacks = completionCallbacks[callbackKey] { callbacks.append(CompletionWrapper(completion)) - completionCallbacks[identifier] = callbacks + completionCallbacks[callbackKey] = callbacks return } else { - completionCallbacks[identifier] = [CompletionWrapper(completion)] + completionCallbacks[callbackKey] = [CompletionWrapper(completion)] } func sampleFileAtURL(_ url: URL, duration: TimeInterval) { let analyser = WaveformAnalyzer(audioAssetURL: url) analyser?.samples(count: numberOfSamples, completionHandler: { samples in // Dispatch back from the WaveformAnalyzer's internal queue - DispatchQueue.main.async { + self.workQueue.async { guard let samples = samples else { self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) return @@ -176,7 +189,9 @@ class VoiceMessageAttachmentCacheManager { } private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) { - guard let callbacks = completionCallbacks[identifier] else { + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: samples.count) + + guard let callbacks = completionCallbacks[callbackKey] else { return } @@ -189,11 +204,13 @@ class VoiceMessageAttachmentCacheManager { } } - self.completionCallbacks[identifier] = nil + self.completionCallbacks[callbackKey] = nil } private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) { - guard let callbacks = completionCallbacks[identifier] else { + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: samples.count) + + guard let callbacks = completionCallbacks[callbackKey] else { return } @@ -204,6 +221,6 @@ class VoiceMessageAttachmentCacheManager { } } - self.completionCallbacks[identifier] = nil + self.completionCallbacks[callbackKey] = nil } } From ec33ec81f823618158acc71b88a0c5e6798bc02e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 14:01:47 +0300 Subject: [PATCH 075/125] #4090 - Fixed the AttachmentCacheManager's serial nature. --- .../VoiceMessageAttachmentCacheManager.swift | 97 ++++++++++++------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 612de8a21d..73d4df484e 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -96,9 +96,12 @@ class VoiceMessageAttachmentCacheManager { } private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result) -> Void) { + MXLog.debug("[VoiceMessageAttachmentCacheManager] Started task") + let callbackKey = CompletionCallbackKey(eventIdentifier: identifier, requiredNumberOfSamples: numberOfSamples) if var callbacks = completionCallbacks[callbackKey] { + MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished task - cached completion callback") callbacks.append(CompletionWrapper(completion)) completionCallbacks[callbackKey] = callbacks return @@ -106,30 +109,37 @@ class VoiceMessageAttachmentCacheManager { completionCallbacks[callbackKey] = [CompletionWrapper(completion)] } + let dispatchGroup = DispatchGroup() + func sampleFileAtURL(_ url: URL, duration: TimeInterval) { let analyser = WaveformAnalyzer(audioAssetURL: url) + + dispatchGroup.enter() analyser?.samples(count: numberOfSamples, completionHandler: { samples in - // Dispatch back from the WaveformAnalyzer's internal queue - self.workQueue.async { - guard let samples = samples else { - self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) - return - } - - if var existingSamples = self.samples[identifier] { - existingSamples[numberOfSamples] = samples - self.samples[identifier] = existingSamples - } else { - self.samples[identifier] = [numberOfSamples: samples] - } - - self.invokeSuccessCallbacksForIdentifier(identifier, url: url, duration: duration, samples: samples) + MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished sampling voice message") + + dispatchGroup.leave() + + guard let samples = samples else { + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError) + return + } + + if var existingSamples = self.samples[identifier] { + existingSamples[numberOfSamples] = samples + self.samples[identifier] = existingSamples + } else { + self.samples[identifier] = [numberOfSamples: samples] } + + self.invokeSuccessCallbacksForIdentifier(identifier, url: url, duration: duration, samples: samples) }) } if let finalURL = finalURLs[identifier], let duration = durations[identifier] { sampleFileAtURL(finalURL, duration: duration) + dispatchGroup.wait() + MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished task") return } @@ -141,11 +151,14 @@ class VoiceMessageAttachmentCacheManager { let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a") + dispatchGroup.enter() VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { result in switch result { case .success: self.finalURLs[identifier] = newURL VoiceMessageAudioConverter.mediaDurationAt(newURL) { result in + MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished converting voice message") + switch result { case .success: if let duration = try? result.get() { @@ -157,35 +170,49 @@ class VoiceMessageAttachmentCacheManager { case .failure(let error): MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed getting audio duration with: \(error)") } + + dispatchGroup.leave() } case .failure(let error): self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error)) MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed decoding audio message with: \(error)") + dispatchGroup.leave() } } } - if attachment.isEncrypted { - attachment.decrypt(toTempFile: { filePath in - convertFileAtPath(filePath) - }, failure: { error in - // A nil error in this case is a cancellation on the MXMediaLoader - if let error = error { - MXLog.error("Failed decrypting attachment with error: \(String(describing: error))") - self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error)) - } - }) - } else { - attachment.prepare({ - convertFileAtPath(attachment.cacheFilePath) - }, failure: { error in - // A nil error in this case is a cancellation on the MXMediaLoader - if let error = error { - MXLog.error("Failed preparing attachment with error: \(String(describing: error))") - self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.preparationError(error)) - } - }) + dispatchGroup.enter() + DispatchQueue.main.async { // These don't behave accordingly if called from a background thread + if attachment.isEncrypted { + attachment.decrypt(toTempFile: { filePath in + convertFileAtPath(filePath) + dispatchGroup.leave() + }, failure: { error in + // A nil error in this case is a cancellation on the MXMediaLoader + if let error = error { + MXLog.error("Failed decrypting attachment with error: \(String(describing: error))") + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error)) + } + dispatchGroup.leave() + }) + } else { + attachment.prepare({ + convertFileAtPath(attachment.cacheFilePath) + dispatchGroup.leave() + }, failure: { error in + // A nil error in this case is a cancellation on the MXMediaLoader + if let error = error { + MXLog.error("Failed preparing attachment with error: \(String(describing: error))") + self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.preparationError(error)) + } + dispatchGroup.leave() + }) + } } + + dispatchGroup.wait() + + MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished task") } private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) { From 089c6889d66a6b8627a940feeb309bd859b08d4b Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 16:57:29 +0300 Subject: [PATCH 076/125] #4090 - Added voice messages switch to the labs section in settings. --- Config/BuildSettings.swift | 2 +- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ Riot/Managers/Settings/RiotSettings.swift | 14 +++++++++++ .../Views/InputToolbar/RoomInputToolbarView.m | 6 ++--- .../Modules/Settings/SettingsViewController.m | 25 ++++++++++++++++++- 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 30e86e015c..9ffad81286 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -311,7 +311,7 @@ final class BuildSettings: NSObject { // MARK: - Voice Message - static let voiceMessagesEnabled = true + static let voiceMessagesEnabled = false // MARK: - HTTP /// Additional HTTP headers will be sent by all requests. Not recommended to use request-specific headers, like `Authorization`. diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 87267e9237..3dbca7ac76 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -534,6 +534,7 @@ Tap the + to start adding people."; "settings_labs_create_conference_with_jitsi" = "Create conference calls with jitsi"; "settings_labs_message_reaction" = "React to messages with emoji"; "settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; +"settings_labs_voice_messages" = "Voice messages"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 6dfc76dd9e..19e61aed8b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4378,6 +4378,10 @@ internal enum VectorL10n { internal static var settingsLabsMessageReaction: String { return VectorL10n.tr("Vector", "settings_labs_message_reaction") } + /// Voice messages + internal static var settingsLabsVoiceMessages: String { + return VectorL10n.tr("Vector", "settings_labs_voice_messages") + } /// Mark all messages as read internal static var settingsMarkAllAsRead: String { return VectorL10n.tr("Vector", "settings_mark_all_as_read") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 533702c2ba..f6260d6482 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -52,6 +52,7 @@ final class RiotSettings: NSObject { static let roomCreationScreenRoomIsPublic = "roomCreationScreenRoomIsPublic" static let allowInviteExernalUsers = "allowInviteExernalUsers" static let enableRingingForGroupCalls = "enableRingingForGroupCalls" + static let enableVoiceMessages = "enableVoiceMessages" static let roomSettingsScreenShowLowPriorityOption = "roomSettingsScreenShowLowPriorityOption" static let roomSettingsScreenShowDirectChatOption = "roomSettingsScreenShowDirectChatOption" static let roomSettingsScreenAllowChangingAccessSettings = "roomSettingsScreenAllowChangingAccessSettings" @@ -92,6 +93,11 @@ final class RiotSettings: NSObject { return userDefaults }() + private override init() { + super.init() + defaults.register(defaults: [UserDefaultsKeys.enableVoiceMessages: BuildSettings.voiceMessagesEnabled]) + } + // MARK: Servers var homeserverUrlString: String { @@ -214,6 +220,14 @@ final class RiotSettings: NSObject { } } + var enableVoiceMessages: Bool { + get { + return defaults.bool(forKey: UserDefaultsKeys.enableVoiceMessages) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.enableVoiceMessages) + } + } + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index e1223b4c7d..2234cbf9af 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -82,7 +82,7 @@ - (void)awakeFromNib - (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView { - if (BuildSettings.voiceMessagesEnabled == NO) { + if (RiotSettings.shared.enableVoiceMessages == NO) { return; } @@ -407,7 +407,7 @@ - (void)setActionMenuOpened:(BOOL)actionMenuOpened [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; - if (BuildSettings.voiceMessagesEnabled) + if (RiotSettings.shared.enableVoiceMessages) { self.voiceMessageToolbarView.alpha = self->growingTextView.text.length > 0 || actionMenuOpened ? 0 : 1; } @@ -443,7 +443,7 @@ - (void)updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated { self.actionMenuOpened = NO; - if (BuildSettings.voiceMessagesEnabled == NO) { + if (RiotSettings.shared.enableVoiceMessages == NO) { self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; self.messageComposerContainerTrailingConstraint.constant = (textMessage.length ? self.frame.size.width - self.rightInputToolbarButton.frame.origin.x : 0.0f) + kComposerContainerTrailingPadding; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index a6dad221f6..92cfd7e613 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -142,7 +142,8 @@ enum { - LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0 + LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0, + LABS_ENABLE_VOICE_MESSAGES = 1 }; enum @@ -487,6 +488,7 @@ - (void)updateSections { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; + [sectionLabs addRowWithTag:LABS_ENABLE_VOICE_MESSAGES]; sectionLabs.headerTitle = NSLocalizedStringFromTable(@"settings_labs", @"Vector", nil); if (sectionLabs.hasAnyRows) { @@ -2263,6 +2265,17 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableRingingForGroupCalls:) forControlEvents:UIControlEventValueChanged]; + cell = labelAndSwitchCell; + } else if (row == LABS_ENABLE_VOICE_MESSAGES) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_labs_voice_messages", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableVoiceMessages; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceMessages:) forControlEvents:UIControlEventValueChanged]; + cell = labelAndSwitchCell; } } @@ -2963,6 +2976,16 @@ - (void)toggleEnableRingingForGroupCalls:(UISwitch *)sender } } +- (void)toggleEnableVoiceMessages:(UISwitch *)sender +{ + if (sender) + { + RiotSettings.shared.enableVoiceMessages = sender.isOn; + + [self.tableView reloadData]; + } +} + - (void)togglePinRoomsWithMissedNotif:(id)sender { UISwitch *switchButton = (UISwitch*)sender; From 0ead60f397b769432b0ee470cf2e6cd13bddf24a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 19 Jul 2021 15:40:17 +0300 Subject: [PATCH 077/125] #4090 - Various tweaks and fixes following code review. Switched back to DateFormatters for formatting durations, sanitising audio player durations and current times. --- .../VoiceMessageAudioPlayer.swift | 22 +---- .../VoiceMessageAudioRecorder.swift | 2 +- .../VoiceMessageController.swift | 29 +++--- .../VoiceMessageMediaServiceProvider.swift | 6 +- .../VoiceMessagePlaybackController.swift | 29 +++--- .../VoiceMessagePlaybackView.swift | 2 +- .../VoiceMessageToolbarView.swift | 2 +- .../VoiceMessageWaveformView.swift | 8 +- .../Modules/Settings/SettingsViewController.m | 97 ++++++------------- 9 files changed, 70 insertions(+), 127 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 1fe9283021..616ed1d347 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -40,7 +40,7 @@ class VoiceMessageAudioPlayer: NSObject { private var statusObserver: NSKeyValueObservation? private var playbackBufferEmptyObserver: NSKeyValueObservation? private var rateObserver: NSKeyValueObservation? - private var playToEndObsever: NSObjectProtocol? + private var playToEndObserver: NSObjectProtocol? private let delegateContainer = DelegateContainer() @@ -55,23 +55,11 @@ class VoiceMessageAudioPlayer: NSObject { } var duration: TimeInterval { - guard let item = self.audioPlayer?.currentItem else { - return 0 - } - - let duration = CMTimeGetSeconds(item.duration) - - return duration.isNaN ? 0.0 : duration + return abs(CMTimeGetSeconds(self.audioPlayer?.currentItem?.duration ?? .zero)) } var currentTime: TimeInterval { - guard let audioPlayer = self.audioPlayer else { - return 0.0 - } - - let currentTime = CMTimeGetSeconds(audioPlayer.currentTime()) - - return currentTime.isNaN ? 0.0 : currentTime + return abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero)) } private(set) var isStopped = true @@ -200,7 +188,7 @@ class VoiceMessageAudioPlayer: NSObject { } } - playToEndObsever = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in + playToEndObserver = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in guard let self = self else { return } self.delegateContainer.notifyDelegatesWithBlock { delegate in @@ -213,7 +201,7 @@ class VoiceMessageAudioPlayer: NSObject { statusObserver?.invalidate() playbackBufferEmptyObserver?.invalidate() rateObserver?.invalidate() - NotificationCenter.default.removeObserver(playToEndObsever as Any) + NotificationCenter.default.removeObserver(playToEndObserver as Any) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index 0ea7a55944..fafabd79ac 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -48,7 +48,7 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { return audioRecorder?.isRecording ?? false } - func recordWithOuputURL(_ url: URL) { + func recordWithOutputURL(_ url: URL) { let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC), AVSampleRateKey: 12000, diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index ea78372ffd..f6dc565771 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -28,6 +28,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private enum Constants { static let maximumAudioRecordingDuration: TimeInterval = 120.0 static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0 + static let elapsedTimeFormat = "m:ss" static let minimumRecordingDuration = 1.0 } @@ -47,6 +48,12 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private var isInLockedMode: Bool = false private var notifiedRemainingTime = false + private static let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = Constants.elapsedTimeFormat + return dateFormatter + }() + @objc public weak var delegate: VoiceMessageControllerDelegate? @objc public var isRecordingAudio: Bool { @@ -90,7 +97,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // Haptic are not played during record on iOS by default. This fix works // only since iOS 13. A workaround for iOS 12 and earlier would be to - // dispatch after at least 100ms recordWithOuputURL call + // dispatch after at least 100ms recordWithOutputURL call if #available(iOS 13.0, *) { try? AVAudioSession.sharedInstance().setCategory(.playAndRecord) try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) @@ -100,7 +107,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioRecorder = mediaServiceProvider.audioRecorder() audioRecorder?.registerDelegate(self) - audioRecorder?.recordWithOuputURL(temporaryFileURL) + audioRecorder?.recordWithOutputURL(temporaryFileURL) } func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { @@ -335,7 +342,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, var details = VoiceMessageToolbarViewDetails() details.state = (isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = durationStringFromTimeInterval(currentTime) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: currentTime)) details.audioSamples = audioSamples if isRecording { @@ -384,7 +391,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, var details = VoiceMessageToolbarViewDetails() details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) - details.elapsedTime = durationStringFromTimeInterval(audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration) + details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration))) details.audioSamples = audioSamples details.isPlaying = audioPlayer.isPlaying details.progress = (audioPlayer.isPlaying ? (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) : 0.0) @@ -399,18 +406,4 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioSamples = audioSamples + [Float](repeating: 0.0, count: delta) } - - private func durationStringFromTimeInterval(_ interval: TimeInterval) -> String { - guard interval.isFinite else { - return "" - } - - var timeInterval = abs(interval) - let hours = trunc(timeInterval / 3600.0) - timeInterval -= hours * 3600.0 - let minutes = trunc(timeInterval / 60.0) - timeInterval -= minutes * 60.0 - - return String(format: "%01.0f:%02.0f", minutes, timeInterval) - } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 82a16c977c..7def8d7e18 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -75,12 +75,12 @@ import Foundation // MARK: - Private private func stopAllServicesExcept(_ service: AnyObject?) { - for audioRecoder in audioRecorders.allObjects { - if audioRecoder === service { + for audioRecorder in audioRecorders.allObjects { + if audioRecorder === service { continue } - audioRecoder.stopRecording() + audioRecorder.stopRecording() } guard let audioPlayersEnumerator = audioPlayers.objectEnumerator() else { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 9abc99e3b3..8fcbc55e49 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -26,6 +26,10 @@ enum VoiceMessagePlaybackControllerState { class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMessagePlaybackViewDelegate { + private enum Constants { + static let elapsedTimeFormat = "m:ss" + } + private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let cacheManager: VoiceMessageAttachmentCacheManager @@ -43,6 +47,13 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } } + private static let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = Constants.elapsedTimeFormat + return dateFormatter + }() + + let playbackView: VoiceMessagePlaybackView init(mediaServiceProvider: VoiceMessageMediaServiceProvider, @@ -134,11 +145,11 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess switch state { case .stopped: - details.currentTime = durationStringFromTimeInterval(self.duration) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: self.duration)) details.progress = 0.0 default: if let audioPlayer = audioPlayer { - details.currentTime = durationStringFromTimeInterval(audioPlayer.currentTime) + details.currentTime = VoiceMessagePlaybackController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioPlayer.currentTime)) details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) } } @@ -199,18 +210,4 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess @objc private func updateTheme() { playbackView.update(theme: ThemeService.shared().theme) } - - private func durationStringFromTimeInterval(_ interval: TimeInterval) -> String { - guard interval.isFinite else { - return "" - } - - var timeInterval = abs(interval) - let hours = trunc(timeInterval / 3600.0) - timeInterval -= hours * 3600.0 - let minutes = trunc(timeInterval / 60.0) - timeInterval -= minutes * 60.0 - - return String(format: "%01.0f:%02.0f", minutes, timeInterval) - } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index e3f53a4347..443168e2f5 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -115,7 +115,7 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { playButton.backgroundColor = theme.colors.background playButton.tintColor = theme.colors.secondaryContent backgroundView.backgroundColor = theme.colors.quinaryContent - _waveformView.primarylineColor = theme.colors.quarterlyContent + _waveformView.primaryLineColor = theme.colors.quarterlyContent _waveformView.secondaryLineColor = theme.colors.secondaryContent elapsedTimeLabel.textColor = theme.colors.tertiaryContent } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 3cfaebff76..4650d6fae6 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -200,7 +200,7 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture case UIGestureRecognizer.State.began: delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) case UIGestureRecognizer.State.ended: - delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) default: break } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift index 9a72c09021..8685109c46 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -28,10 +28,10 @@ class VoiceMessageWaveformView: UIView { } } - var primarylineColor = UIColor.lightGray { + var primaryLineColor = UIColor.lightGray { didSet { - backgroundLayer.strokeColor = primarylineColor.cgColor - backgroundLayer.fillColor = primarylineColor.cgColor + backgroundLayer.strokeColor = primaryLineColor.cgColor + backgroundLayer.fillColor = primaryLineColor.cgColor } } var secondaryLineColor = UIColor.darkGray { @@ -60,7 +60,7 @@ class VoiceMessageWaveformView: UIView { override init(frame: CGRect) { super.init(frame: frame) - setupAndAdd(backgroundLayer, with: primarylineColor) + setupAndAdd(backgroundLayer, with: primaryLineColor) setupAndAdd(progressLayer, with: secondaryLineColor) progressLayer.masksToBounds = true diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 92cfd7e613..11bbcb3d87 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2802,7 +2802,7 @@ - (void)onRemove3PID:(NSIndexPath*)indexPath } } -- (void)togglePushNotifications:(id)sender +- (void)togglePushNotifications:(UISwitch *)sender { // Check first whether the user allow notification from device settings UIUserNotificationType currentUserNotificationTypes = UIApplication.sharedApplication.currentUserNotificationSettings.types; @@ -2832,7 +2832,7 @@ - (void)togglePushNotifications:(id)sender [self presentViewController:currentAlert animated:YES completion:nil]; // Keep off the switch - ((UISwitch*)sender).on = NO; + sender.on = NO; } else if ([MXKAccountManager sharedManager].activeAccounts.count) { @@ -2855,7 +2855,7 @@ - (void)togglePushNotifications:(id)sender [[AppDelegate theDelegate] registerForRemoteNotificationsWithCompletion:^(NSError * error) { if (error) { - [(UISwitch *)sender setOn:NO animated:YES]; + [sender setOn:NO animated:YES]; [self stopActivityIndicator]; } else @@ -2871,49 +2871,42 @@ - (void)togglePushNotifications:(id)sender } } -- (void)toggleCallKit:(id)sender +- (void)toggleCallKit:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - [MXKAppSettings standardAppSettings].enableCallKit = switchButton.isOn; + [MXKAppSettings standardAppSettings].enableCallKit = sender.isOn; } -- (void)toggleStunServerFallback:(id)sender +- (void)toggleStunServerFallback:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - RiotSettings.shared.allowStunServerFallback = switchButton.isOn; + RiotSettings.shared.allowStunServerFallback = sender.isOn; self.mainSession.callManager.fallbackSTUNServer = RiotSettings.shared.allowStunServerFallback ? BuildSettings.stunServerFallbackUrlString : nil; } -- (void)toggleAllowIntegrations:(id)sender +- (void)toggleAllowIntegrations:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - MXSession *session = self.mainSession; [self startActivityIndicator]; - + __block RiotSharedSettings *sharedSettings = [[RiotSharedSettings alloc] initWithSession:session]; - [sharedSettings setIntegrationProvisioningWithEnabled:switchButton.on success:^{ + [sharedSettings setIntegrationProvisioningWithEnabled:sender.isOn success:^{ sharedSettings = nil; [self stopActivityIndicator]; } failure:^(NSError * _Nullable error) { sharedSettings = nil; - [switchButton setOn:!switchButton.on animated:YES]; + [sender setOn:!sender.isOn animated:YES]; [self stopActivityIndicator]; }]; } -- (void)toggleShowDecodedContent:(id)sender +- (void)toggleShowDecodedContent:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - RiotSettings.shared.showDecryptedContentInNotifications = switchButton.isOn; + RiotSettings.shared.showDecryptedContentInNotifications = sender.isOn; } -- (void)toggleLocalContactsSync:(id)sender +- (void)toggleLocalContactsSync:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - - if (switchButton.on) + if (sender.on) { [MXKContactManager requestUserConfirmationForLocalContactsSyncInViewController:self completionHandler:^(BOOL granted) { @@ -2954,57 +2947,36 @@ - (void)toggleSendCrashReport:(id)sender } } -- (void)toggleEnableRageShake:(id)sender +- (void)toggleEnableRageShake:(UISwitch *)sender { - if (sender && [sender isKindOfClass:UISwitch.class]) - { - UISwitch *switchButton = (UISwitch*)sender; - - RiotSettings.shared.enableRageShake = switchButton.isOn; - - [self updateSections]; - } + RiotSettings.shared.enableRageShake = sender.isOn; + + [self updateSections]; } - (void)toggleEnableRingingForGroupCalls:(UISwitch *)sender { - if (sender) - { - RiotSettings.shared.enableRingingForGroupCalls = sender.isOn; - - [self.tableView reloadData]; - } + RiotSettings.shared.enableRingingForGroupCalls = sender.isOn; } - (void)toggleEnableVoiceMessages:(UISwitch *)sender { - if (sender) - { - RiotSettings.shared.enableVoiceMessages = sender.isOn; - - [self.tableView reloadData]; - } + RiotSettings.shared.enableVoiceMessages = sender.isOn; } -- (void)togglePinRoomsWithMissedNotif:(id)sender +- (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - - RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = switchButton.on; + RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; } -- (void)togglePinRoomsWithUnread:(id)sender +- (void)togglePinRoomsWithUnread:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - - RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome = switchButton.on; + RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome = sender.on; } -- (void)toggleCommunityFlair:(id)sender +- (void)toggleCommunityFlair:(UISwitch *)sender { - UISwitch *switchButton = (UISwitch*)sender; - - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:switchButton.tag inSection:groupsDataSource.joinedGroupsSection]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:sender.tag inSection:groupsDataSource.joinedGroupsSection]; id groupCellData = [groupsDataSource cellDataAtIndex:indexPath]; MXGroup *group = groupCellData.group; @@ -3014,7 +2986,7 @@ - (void)toggleCommunityFlair:(id)sender __weak typeof(self) weakSelf = self; - [self.mainSession updateGroupPublicity:group isPublicised:switchButton.on success:^{ + [self.mainSession updateGroupPublicity:group isPublicised:sender.isOn success:^{ if (weakSelf) { @@ -3030,7 +3002,7 @@ - (void)toggleCommunityFlair:(id)sender [self stopActivityIndicator]; // Come back to previous state button - [switchButton setOn:!switchButton.isOn animated:YES]; + [sender setOn:!sender.isOn animated:YES]; // Notify user [[AppDelegate theDelegate] showErrorAsAlert:error]; @@ -3676,16 +3648,9 @@ - (void)showInviteFriendsFromSourceView:(UIView*)sourceView animated:YES]; } -- (void)toggleNSFWPublicRoomsFiltering:(id)sender +- (void)toggleNSFWPublicRoomsFiltering:(UISwitch *)sender { - if (sender && [sender isKindOfClass:UISwitch.class]) - { - UISwitch *switchButton = (UISwitch*)sender; - - RiotSettings.shared.showNSFWPublicRooms = switchButton.isOn; - - [self.tableView reloadData]; - } + RiotSettings.shared.showNSFWPublicRooms = sender.isOn; } #pragma mark - TextField listener From 39084b00da2286e4ecdb8f58feaac2370d35b25f Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 19 Jul 2021 15:54:14 +0300 Subject: [PATCH 078/125] #4090 - Activating the shared AVAudioSession before recording or playback. --- Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift | 1 + Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 616ed1d347..c4bb0880ee 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -97,6 +97,7 @@ class VoiceMessageAudioPlayer: NSObject { do { try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) + try AVAudioSession.sharedInstance().setActive(true) } catch { MXLog.error("Could not redirect audio playback to speakers.") } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index fafabd79ac..76043f073d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -57,6 +57,7 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { do { try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default) + try AVAudioSession.sharedInstance().setActive(true) audioRecorder = try AVAudioRecorder(url: url, settings: settings) audioRecorder?.delegate = self audioRecorder?.isMeteringEnabled = true From 34f9fb433f7c380ef04a7baa46013649c5cb6449 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 21 Jul 2021 16:19:06 +0300 Subject: [PATCH 079/125] Fixes #4583 - Mention user does not work (settings -> members -> select a member -> mention) --- CHANGES.rst | 2 +- Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift | 3 ++- .../RoomInfoCoordinatorBridgePresenter.swift | 5 +++++ .../Room/RoomInfo/RoomInfoCoordinatorType.swift | 1 + Riot/Modules/Room/RoomViewController.m | 12 +++++------- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index be5868916b..6920f8087e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,7 @@ Changes to be released in next version * 🐛 Bugfix - * + * Room: Fixed mentioning users from room info member details (#4583) ⚠️ API Changes * diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 495d3468bb..569803fbf1 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -194,7 +194,8 @@ extension RoomInfoCoordinator: RoomInfoListCoordinatorDelegate { extension RoomInfoCoordinator: RoomParticipantsViewControllerDelegate { func roomParticipantsViewController(_ roomParticipantsViewController: RoomParticipantsViewController!, mention member: MXRoomMember!) { - + self.navigationRouter.popToRootModule(animated: true) + self.delegate?.roomInfoCoordinator(self, didRequestMentionForMember: member) } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift index 44a46cbd05..48dc70d3e6 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift @@ -20,6 +20,7 @@ import Foundation @objc protocol RoomInfoCoordinatorBridgePresenterDelegate { func roomInfoCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter) + func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didRequestMentionForMember member: MXRoomMember) } /// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller. @@ -115,6 +116,10 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate { self.delegate?.roomInfoCoordinatorBridgePresenterDelegateDidComplete(self) } + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) { + self.delegate?.roomInfoCoordinatorBridgePresenter(self, didRequestMentionForMember: member) + } + } // MARK: - UIAdaptivePresentationControllerDelegate diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift index ff2e046265..092a3b8748 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift @@ -20,6 +20,7 @@ import Foundation protocol RoomInfoCoordinatorDelegate: AnyObject { func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType) + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) } /// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index fbd8a25f09..d4f7c05b39 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3936,13 +3936,6 @@ - (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView*)toolbarView [self cancelEventSelection]; } -#pragma mark - RoomParticipantsViewControllerDelegate - -- (void)roomParticipantsViewController:(RoomParticipantsViewController *)roomParticipantsViewController mention:(MXRoomMember*)member -{ - [self mention:member]; -} - #pragma mark - MXKRoomMemberDetailsViewControllerDelegate - (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion @@ -6121,6 +6114,11 @@ - (void)roomInfoCoordinatorBridgePresenterDelegateDidComplete:(RoomInfoCoordinat self.roomInfoCoordinatorBridgePresenter = nil; } +- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didRequestMentionForMember:(MXRoomMember *)member +{ + [self mention:member]; +} + #pragma mark - RemoveJitsiWidgetViewDelegate - (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view From 54e7b547491c55ed24be22a1805641e87a553a00 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 22 Jul 2021 12:36:29 +0200 Subject: [PATCH 080/125] finish version++ From 24fd98ea01734c20d521d0765e0c5a31f5fc6078 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 22 Jul 2021 12:36:33 +0200 Subject: [PATCH 081/125] Prepare for new sprint --- CHANGES.rst | 24 ++++++++++++++++++++++++ Config/AppIdentifiers.xcconfig | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7bb3eb3504..99bedb8249 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,27 @@ +Changes to be released in next version +================================================= + +✨ Features + * + +🙌 Improvements + * + +🐛 Bugfix + * + +⚠️ API Changes + * + +🗣 Translations + * + +🧱 Build + * + +Others + * + Changes in 1.4.7 (2021-07-22) ================================================= diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index a5e2e60c98..1628e83d86 100644 --- a/Config/AppIdentifiers.xcconfig +++ b/Config/AppIdentifiers.xcconfig @@ -22,8 +22,8 @@ APPLICATION_GROUP_IDENTIFIER = group.im.vector APPLICATION_SCHEME = element // Version -MARKETING_VERSION = 1.4.7 -CURRENT_PROJECT_VERSION = 1.4.7 +MARKETING_VERSION = 1.4.8 +CURRENT_PROJECT_VERSION = 1.4.8 // Team From adcab16e190a4895c327b258e5e9f8c4d96b27c4 Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 22 Jul 2021 14:47:37 +0200 Subject: [PATCH 082/125] Update Podfile.lock --- Podfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index b2d0ed3c53..8a8b129b91 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -116,7 +116,7 @@ PODS: DEPENDENCIES: - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - DSWaveformImage (~> 6.1.1) - - ffmpeg-kit-ios-audio (~> 4.4) + - ffmpeg-kit-ios-audio (~> 4.4.LTS) - FLEX (~> 4.4.1) - FlowCommoniOS (~> 1.10.0) - GBDeviceInfo (~> 6.6.0) @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 6e4ab90565384faccee0cb985abe05663c36f517 +PODFILE CHECKSUM: c7386ecfb38fc4302613c915aef79eebdb98a53d COCOAPODS: 1.10.1 From 4ad5036d143e52111bcc054e804f8353e7727090 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 22 Jul 2021 14:44:34 +0100 Subject: [PATCH 083/125] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5412eb511e..4a05511968 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ Changes to be released in next version * Others - * + * Separated CI jobs into individual actions Changes in 1.4.7 (2021-07-22) ================================================= From b92f800f37854d8730ed4e04133d2bd61c9d0bdd Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 22 Jul 2021 14:54:02 +0100 Subject: [PATCH 084/125] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5412eb511e..9ed3a5e106 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ Changes to be released in next version * Others - * + * Update Podfile.lock Changes in 1.4.7 (2021-07-22) ================================================= From 273f4456a7aad3fd27a83b042b9a632f0fbf6153 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:03:10 +0100 Subject: [PATCH 085/125] Update CHANGES.rst Co-authored-by: manuroe --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9ed3a5e106..05a4a4eeca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ Changes to be released in next version * Others - * Update Podfile.lock + * Update Gemfile.lock Changes in 1.4.7 (2021-07-22) ================================================= From 892f6d7718c6cf92096b067c3ac6816229f15bb7 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 23 Jul 2021 10:42:08 +0200 Subject: [PATCH 086/125] RecentsDataSource: Factorize section reset management and do not make it in refreshRoomsSection method. --- .../Recents/DataSources/RecentsDataSource.m | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 0e2f14ea38..123d9da690 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -71,17 +71,9 @@ - (instancetype)init processingQueue = dispatch_queue_create("RecentsDataSource", DISPATCH_QUEUE_SERIAL); _crossSigningBannerDisplay = CrossSigningBannerDisplayNone; - crossSigningBannerSection = -1; - _secureBackupBannerDisplay = SecureBackupBannerDisplayNone; - secureBackupBannerSection = -1; - directorySection = -1; - invitesSection = -1; - favoritesSection = -1; - peopleSection = -1; - conversationSection = -1; - lowPrioritySection = -1; - serverNoticeSection = -1; + + [self resetSectionIndexes]; _areSectionsShrinkable = NO; shrinkedSectionsBitMask = 0; @@ -96,6 +88,19 @@ - (instancetype)init return self; } +- (void)resetSectionIndexes +{ + crossSigningBannerSection = -1; + secureBackupBannerSection = -1; + directorySection = -1; + invitesSection = -1; + favoritesSection = -1; + peopleSection = -1; + conversationSection = -1; + lowPrioritySection = -1; + serverNoticeSection = -1; +} + #pragma mark - Properties @@ -445,7 +450,7 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView // Check whether all data sources are ready before rendering recents if (self.state == MXKDataSourceStateReady) { - crossSigningBannerSection = secureBackupBannerSection = directorySection = favoritesSection = peopleSection = conversationSection = lowPrioritySection = invitesSection = serverNoticeSection = -1; + [self resetSectionIndexes]; if (self.crossSigningBannerDisplay != CrossSigningBannerDisplayNone) { @@ -1119,10 +1124,8 @@ - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSe #pragma mark - MXKDataSourceDelegate -- (void)refreshRoomsSection:(void (^)(void))onComplete; +- (void)refreshRoomsSection:(void (^)(void))onComplete { - secureBackupBannerSection = directorySection = favoritesSection = peopleSection = conversationSection = lowPrioritySection = serverNoticeSection = invitesSection = -1; - if (displayedRecentsDataSourceArray.count > 0) { // FIXME manage multi accounts From 3231dc0ac509726c7bfd76e2da13a0391ca04786 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 23 Jul 2021 10:43:58 +0200 Subject: [PATCH 087/125] Update changes --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 49dae406ae..7001f18228 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Changes to be released in next version 🙌 Improvements * Room: Added support for Voice Messages (#4090, #4091, #4092, #4094, #4095, #4096) * Remove the directory section from the Rooms tab. + * RecentsDataSource: Factorize section reset in one place. 🐛 Bugfix * Room: Fixed mentioning users from room info member details (#4583) From 045c316a59a7c23b62eedeb57693323aea6c76cd Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 23 Jul 2021 10:19:42 +0100 Subject: [PATCH 088/125] Update CHANGES.rst. --- CHANGES.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index be091fc8ca..e0140c1531 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,6 @@ Changes to be released in next version * Remove the directory section from the Rooms tab. * Notifications: Show decrypted content is enabled by default (#4519). - 🐛 Bugfix * Room: Fixed mentioning users from room info member details (#4583) From 1f88d999bb1103b58924ae589c64bbaed838bc6a Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 23 Jul 2021 14:29:27 +0200 Subject: [PATCH 089/125] Update changes --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7001f18228..81bdbedb3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,7 +7,7 @@ Changes to be released in next version 🙌 Improvements * Room: Added support for Voice Messages (#4090, #4091, #4092, #4094, #4095, #4096) * Remove the directory section from the Rooms tab. - * RecentsDataSource: Factorize section reset in one place. + * RecentsDataSource: Factorize section reset in one place (target #4591). 🐛 Bugfix * Room: Fixed mentioning users from room info member details (#4583) From fbb228a1ae6ac7e466ce47eae3d30b9b739a22f1 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 23 Jul 2021 14:08:51 +0100 Subject: [PATCH 090/125] Configure identity server keyboard for safer URL entry. --- ...ngsIdentityServerViewController.storyboard | 19 ++++++++----------- ...SettingsIdentityServerViewController.swift | 1 - 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard index ab35e9993b..b33325642b 100644 --- a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard +++ b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -19,7 +17,7 @@ - + @@ -38,9 +36,8 @@ - - + @@ -63,7 +60,7 @@ -