From 9448a6fc15e4d8223147ef70a6e2063b0900465e 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 000000000..ead86edbb --- /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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; 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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; { 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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [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 aef8b2f81..a1daff487 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 000000000..6537c1b97 --- /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 000000000..a2f71b9d9 --- /dev/null +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceRecordView.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d71c6027bf6abfb3da81ddca6bf74dedcb27c63d 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 000000000..73c00596a --- /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 000000000..a9f8f0bba --- /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 c1fc9b6b6..7588bee9c 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 b3d7ad726..f1eb8c67c 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 36b65a1da..d6a8d9e1e 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -35,14 +35,15 @@ const CGFloat kActionMenuAttachButtonSpringDamping = .45; 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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; 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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; 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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; - (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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [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 a1daff487..a170d73e0 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 000000000..5b56b653e --- /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 000000000..2b7d80a62 --- /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 6537c1b97..000000000 --- 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 a2f71b9d9..000000000 --- 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 000000000..b101c7ea5 --- /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 8a58224ed965eb034d8d4a7d5df52428c6fe6000 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 a9f8f0bba..1d88a1deb 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 7588bee9c..4888dcb5d 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 d6a8d9e1e..fe573c6b1 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -41,7 +41,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; UIAlertController *actionSheet; } -@property (nonatomic, weak) IBOutlet UIView *voiceMessageToolbarContainerView; @property (nonatomic, strong) VoiceMessageToolbarView *voiceMessageToolbarView; @end @@ -80,9 +79,13 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; 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 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [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 a170d73e0..1056cd289 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 5b56b653e..959b63a74 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 2b7d80a62..e74a79442 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 bfbf0fa7137b3ba1509dcdcfb91a458021c6aef5 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 e74a79442..cb7a3ae30 100644 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib @@ -19,7 +19,7 @@ - + From 11fb964e90764529c97a4628fba4c762d4516ab1 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 fb4b7db8f..9869b17a3 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -135,7 +135,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate> { // The preview header @@ -240,6 +240,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; +@property (nonatomic, strong, readonly) VoiceMessageController *voiceMessageController; + @end @implementation RoomViewController @@ -313,6 +315,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Show / hide actions button in document preview according BuildSettings self.allowActionsInDocumentPreview = BuildSettings.messageDetailsAllowShare; + + _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared]; + self.voiceMessageController.delegate = self; } - (void)viewDidLoad @@ -1114,6 +1119,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (!self.inputToolbarView || ![self.inputToolbarView isMemberOfClass:roomInputToolbarViewClass]) { [super setRoomInputToolbarViewClass:roomInputToolbarViewClass]; + + [(RoomInputToolbarView *)self.inputToolbarView setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView]; + [self updateInputToolBarViewHeight]; } } @@ -6153,4 +6161,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]; } +#pragma mark - VoiceMessageControllerDelegate + +- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController didRequestSendForFileAtURL:(NSURL *)url completion:(void (^)(BOOL))completion +{ + [self.roomDataSource sendAudioFile:url mimeType:@"audio/mp4" success:^(NSString *eventId) { + MXLogDebug(@"Success with event id %@", eventId); + completion(YES); + } failure:^(NSError *error) { + MXLogError(@"Failed sending voice message"); + completion(NO); + }]; +} + @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index f1eb8c67c..4c682b0dd 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -67,6 +67,7 @@ typedef enum : NSUInteger @property (weak, nonatomic) IBOutlet UILabel *inputContextLabel; @property (weak, nonatomic) IBOutlet UIButton *inputContextButton; @property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar; +@property (weak, nonatomic) UIView *voiceMessageToolbarView; /** 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 fe573c6b1..585cde910 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -41,8 +41,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; UIAlertController *actionSheet; } -@property (nonatomic, strong) VoiceMessageToolbarView *voiceMessageToolbarView; - @end @implementation RoomInputToolbarView @@ -78,16 +76,19 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; self.isEncryptionEnabled = _isEncryptionEnabled; - self.voiceMessageToolbarView = [VoiceMessageToolbarView instanceFromNib]; + [self _updateUIWithTextMessage:nil animated:NO]; +} + +- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView +{ + _voiceMessageToolbarView = 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]; } #pragma mark - Override MXKView @@ -140,8 +141,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; self.inputContextLabel.textColor = ThemeService.shared.theme.textSecondaryColor; self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor; [self.actionsBar updateWithTheme:ThemeService.shared.theme]; - - [self.voiceMessageToolbarView updateWithTheme:ThemeService.shared.theme]; } #pragma mark - diff --git a/Riot/Modules/Room/VoiceMessages/AudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/AudioPlayer.swift new file mode 100644 index 000000000..a51e13afe --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/AudioPlayer.swift @@ -0,0 +1,17 @@ +// +// 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 diff --git a/Riot/Modules/Room/VoiceMessages/AudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/AudioRecorder.swift new file mode 100644 index 000000000..ebd390b7c --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/AudioRecorder.swift @@ -0,0 +1,82 @@ +// +// 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 AVFoundation + +protocol AudioRecorderDelegate: AnyObject { + func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) + func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) + func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) +} + +enum AudioRecorderError: Error { + case genericError +} + +class AudioRecorder: NSObject, AVAudioRecorderDelegate { + + private(set) var isRecording: Bool = false + private(set) var currentTime: TimeInterval = 0 + + var url: URL? { + return audioRecorder?.url + } + private var audioRecorder: AVAudioRecorder? + + weak var delegate: AudioRecorderDelegate? + + func recordWithOuputURL(_ url: URL) { + + let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC), + AVSampleRateKey: 12000, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue] + + do { + try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default) + audioRecorder = try AVAudioRecorder(url: url, settings: settings) + audioRecorder?.delegate = self + audioRecorder?.record() + delegate?.audioRecorderDidStartRecording(self) + } catch { + delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError) + } + + } + + func stopRecording() { + audioRecorder?.stop() + } + + // MARK: - AVAudioRecorderDelegate + + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully success: Bool) { + if success { + delegate?.audioRecorderDidFinishRecording(self) + } else { + delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError) + } + } + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError) + } +} + +extension String: LocalizedError { + public var errorDescription: String? { return self } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift new file mode 100644 index 000000000..c15abd881 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -0,0 +1,109 @@ +// +// 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 protocol VoiceMessageControllerDelegate: AnyObject { + func voiceMessageController(_ voiceMessageController: VoiceMessageController, didRequestSendForFileAtURL url: URL, completion: @escaping (Bool) -> 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 959b63a74..a01a0ff7d 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 b101c7ea5..2d89fc8f7 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 04f2ad7a6e0dbd739b30e62ef2b805f484357edc 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 000000000..521b5c2dc --- /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 a01a0ff7d..70816fefb 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 cb7a3ae30..55946887d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -23,78 +23,96 @@ - + - - + + - - + + - - + + + + - - - - - - - - - - - - - - + + - + + - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -109,5 +127,6 @@ + From 19e402a6229842bcfb7733776e1007f8327adc1d 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 6746493b4..0b963c87f 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 000000000..21dd49f04 --- /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 000000000..2edca4032 --- /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 922dae9fa..a96e4c1af 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 b853631cd..4dfbe686f 100644 --- a/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m +++ b/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m @@ -112,6 +112,9 @@ 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 423b84426..2a216c44f 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -986,6 +986,9 @@ static NSAttributedString *timestampVerticalWhitespace = nil; 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 e37699e73..e4b2d4433 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -391,6 +391,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [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 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { 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 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; #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 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [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 000000000..eaeaf722b --- /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 000000000..b7e516675 --- /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 a51e13afe..cc091b39d 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 000000000..640d54e5e --- /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 9ba2161f5..4bf56db84 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 8623daad1..94306a5ae 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 000000000..675f192f0 --- /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 000000000..72f371dec --- /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 70816fefb..5d7c567be 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 000000000..3116f30eb --- /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 2c65088ea12ad3818f39bcd2b025d6239442a15d 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 3cfcf65dc..32af3872a 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 585cde910..dfbc1b4b0 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -81,6 +81,10 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; - (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView { + if (BuildSettings.voiceMessagesEnabled == NO) { + return; + } + _voiceMessageToolbarView = voiceMessageToolbarView; self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:self.voiceMessageToolbarView]; @@ -434,6 +438,10 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; { 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 78958e237252b82a75b68dbce455c4d7d8297f68 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 000000000..183d15e30 --- /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 000000000..c5f013138 --- /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 21dd49f04..f0d8168f9 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 8f7b664f9..d46493b81 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 6218ad96a..fe548450b 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 39c159e04..8a7a65dff 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 8d9bcc0b51760f05a5e425a3981a429709480bc8 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 b00d68d82..faa1da2b4 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 512c17018..2ec395bd1 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 d46493b81..65da2738a 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 000000000..d862f4861 --- /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 ead91f878157e36887d771150d66d5d562d21f8e 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 804afbfe7..9bafef2da 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 635e5ea40..7b1f8231a 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 faa1da2b4..666acde8e 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 2ec395bd1..63fedd65a 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 ba0bd2654..466d11903 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 65da2738a..106038fc0 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 fe548450b..fb56fcd18 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 2d89fc8f7..b7dad91b0 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 d862f4861..a1a206abb 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 37e9ca52c15dd2032e0bf160f282222e6f73f93a 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 d2b0b1875..4d0786654 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -316,7 +316,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // 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 09108d0c9..db7eb1b8f 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 9bafef2da..76f5dfbfc 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 4401d11b3..0ea7a5594 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 b65d66d15..142536c37 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 000000000..3c192a679 --- /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 a624c083e..c89f7a6e1 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 000000000..c4323e980 --- /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 02e325ecabbffc6bbb35b1eb6b50cab635eb790e 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 142536c37..ce96062b2 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 c89f7a6e1..836204029 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 705c2290f3f2b1a4bd676733a59087781d03ad36 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 5772467ee..c3c50db54 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 0b963c87f..32d841a90 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 4d0786654..5d23e8fa6 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6187,7 +6187,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (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 000000000..2df5454cb --- /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 ce96062b2..d3e044ce3 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 836204029..dedbd3919 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 3292be7d4..3333b1805 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 0b155b987980c99bc611f8e60ccf35b18604b9bb 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 234022cf2..b15502a61 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 ba05c0233..ddf58c6bb 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 d3e044ce3..f0d8a9158 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 dedbd3919..3a4c59057 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 172208622..ccb80f522 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 15f3641c0..902ce9bf2 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -206,16 +206,36 @@ + + + + + + + + + + + + + + + @@ -245,6 +265,8 @@ + + From 188b4d1346392c4bbf1707c4fda9c80a08a25f9d 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 | 98 ++-------- 4 files changed, 209 insertions(+), 79 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 5d23e8fa6..c4fab0c29 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -616,6 +616,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; 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 db7eb1b8f..dbf36d15e 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 000000000..1054c8232 --- /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 3a4c59057..9a438f707 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") - - 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(url) - let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() - if requiredNumberOfSamples == 0 { - return + cacheManager.loadAttachment(attachment, numberOfSamples: requiredNumberOfSamples) { result in + switch result { + case .success(let result): + self.audioPlayer.loadContentFromURL(result.0) + self.samples = result.1 + self.updateUI() + case .failure: + self.state = .error + } } - - 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 07873b196265b47fbd6414c56e95c9dfb8af232d 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 f0d8a9158..1b072e445 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 9a438f707..dd95edd7e 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 a1a206abb..5cd2e2eb1 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 +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 responds(to aSelector: Selector!) -> Bool { - return (wrappedObject?.responds(to: aSelector) ?? false) || super.responds(to: aSelector) - } - - override func forwardingTarget(for aSelector: Selector!) -> Any? { - return wrappedObject + + @objc private func handleTick(parameter: Any) { + _ = self.target?.perform(self.selector, with: parameter) } } From 36cc7d5b8d49e87c0558580967dafa5594e29f29 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 1054c8232..f1fd66efd 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 1b072e445..3f8a61a12 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 1d281852aa567cf5e3c8121541b795a7a7baf1f5 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 f1fd66efd..354694172 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 2df5454cb..87e932b60 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 3333b1805..3292be7d4 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 a7fa0d2413ffda042566fb37d077b34a801302fe 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 ccb80f522..b373aaccb 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 902ce9bf2..f11674470 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -216,7 +216,7 @@ - + From a3e8a9c114bbc836ea335f6c20586ca4e4c036c9 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 3f8a61a12..9c2d500e5 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 b373aaccb..9a37e9917 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 4cf66469318f32cc0a7852e77007a827a6ca9cd4 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 354694172..54fc6d9fc 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 a9349d86f4252f47a488dab81d85aeeea6f74da1 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 4c682b0dd..430daa2bf 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 65bdfce99..fb0460ad8 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -34,6 +34,7 @@ const CGFloat kActionMenuAttachButtonSpringVelocity = 7; const CGFloat kActionMenuAttachButtonSpringDamping = .45; const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; +const CGFloat kComposerContainerTrailingPadding = 12; @interface RoomInputToolbarView() { @@ -439,6 +440,11 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; 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 1056cd289..f0f0c35b3 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 bb3312c423ad3f62b062aee9a11d6d16ec97a856 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 fb0460ad8..e1223b4c7 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -407,6 +407,10 @@ const CGFloat kComposerContainerTrailingPadding = 12; [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 471435fccf11fb41398bfe4fe8f7c509eda6d746 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 54fc6d9fc..b50067638 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 9c2d500e5..662800be8 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 ba05555bf2bfaa8c49fec06f679551e33a3f926d 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 b5dc66261..fc96f3608 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 a1cf2ac54..39a3b0893 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 d8b5e108b..53a9566e2 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 ff9da941a..019c73454 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 e0aafe44027246047b2b688badb64db98c009f8d 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 | 129 ++++++++++-------- 6 files changed, 146 insertions(+), 81 deletions(-) diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 84ecb908a..a9bea1d96 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 87e932b60..6c8f5cadb 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 dd95edd7e..f3a4af603 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 019c73454..4eeb01242 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 8a7a65dff..d18023630 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() + + let drawMappingFactor = self.bounds.size.height + let minimumGraphAmplitude: CGFloat = 1 + + 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) + + path.move(to: CGPoint(x: xOffset, y: self.bounds.midY - drawingAmplitude / 2)) + path.addLine(to: CGPoint(x: xOffset, y: self.bounds.midY + drawingAmplitude / 2)) + + xOffset += self.lineWidth + self.linePadding + + index += 1 + } + + DispatchQueue.main.async { + self.backgroundLayer.path = path.cgPath + self.progressLayer.path = path.cgPath + } } - - 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) - } + + 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 d6c0be5a6b5203faa566b8cbadb8956a03d79f51 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 ff81e5f57..be5ad0b46 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -4201,18 +4201,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [[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 4c2bd1cc9a3b071d67f1e9478a058b4050f5277b 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 2e72d9c8e..575f22768 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 b24879ac6691b578fb286a17c931ee3b27ae96a8 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 | 66 +++++++++++++------ .../VoiceMessageWaveformView.swift | 5 +- 5 files changed, 93 insertions(+), 108 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 03f27227f..30704b6fa 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 662800be8..233f9e7e7 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 3c192a679..7d455c0a3 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 f3a4af603..f204f3e5c 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() - } else { - if let urlToLoad = urlToLoad { - audioPlayer.loadContentFromURL(urlToLoad) + if mediaServiceProvider.mediaIdentifier == attachment?.eventId { + if mediaServiceProvider.audioPlayer.isPlaying { + mediaServiceProvider.audioPlayer.pause() + } else { + mediaServiceProvider.audioPlayer.registerDelegate(self) + mediaServiceProvider.audioPlayer.play() + } + } else { + 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 d18023630..c0abee843 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 15a90b31e75b730aa43a47c5e21df63faa937614 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 679b2f8f0..5e10fa45c 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 bc3de1782528db6f4f5aaec2c10567c58b98b43a 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 c4fab0c29..3338f979b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -240,7 +240,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; -@property (nonatomic, strong, readonly) VoiceMessageController *voiceMessageController; +@property (nonatomic, strong) VoiceMessageController *voiceMessageController; @end From 6f776e83326d57f8875896b07afe83b606da7eba Mon Sep 17 00:00:00 2001 From: Gil Eluard 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 b15502a61..eef3e9aa3 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 0c5bbbe474d0e05829fe1825c9b204b94b06ea23 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 5985ce346..4ec4b2d92 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 @@ [super finalizeInit]; directRoomsSectionNumber = 0; - contactsSectionNumber = 0; self.screenName = @"People"; } @@ -92,35 +84,9 @@ // 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 @@ { 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 @@ 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 @@ { 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 @@ { 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 @@ - (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 @@ - (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 @@ - (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 @@ - (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 @@ 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 @@ 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 @@ } } -#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 @@ { 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 6ef661948..b558fc110 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 b5526e4496a9da4a5188f8f8d638afcde90fbb47 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 b558fc110..6ef661948 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 b9b63ff03e280bbadd01ac69c9429fe0c614abba 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 4ec4b2d92..79cbf86e7 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -145,49 +145,43 @@ - (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 @@ - (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 @@ - (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 @@ - (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 @@ - (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 113fd85103f80ae222760e517fec12af2d04e44e 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 4dfc32ac7..6e491fdca 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 a85ea69be..11a0fa5a1 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -397,7 +397,7 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = } // 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 ddf58c6bb..57f016874 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 dbf36d15e..397fc9fe7 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 30704b6fa..d15a0c990 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 6c8f5cadb..3d4ef1df6 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 233f9e7e7..a574b8bb5 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 f204f3e5c..57aa4bba8 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 4eeb01242..c749bcd47 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 5cd2e2eb1..695df92e8 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 f9f3dad1fac434a24c13056a2146ddd85559f70e 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 3cfcf65dc..180c59033 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 3857f61fc..a28236c04 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -483,13 +483,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou conversationSection = sectionsCount++; } - if (_recentsDataSourceMode == RecentsDataSourceModeRooms - && BuildSettings.publicRoomsShowDirectory) - { - // Add the directory section after "ROOMS" - directorySection = sectionsCount++; - } - if (self.lowPriorityCellDataArray.count > 0) { lowPrioritySection = sectionsCount++; From 0aefc5db381a49b29a804b67db162f656bd2d5f5 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 c562c4d0b..0fc069dcc 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 a9dc73b5d..b3c820b73 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 @@ [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 @@ 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 @@ } } -#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 @@ - (NSUInteger)totalItemCounts { return recentsDataSource.conversationCellDataArray.count - + recentsDataSource.publicRoomsDirectoryDataSource.roomsCount + recentsDataSource.invitesCellDataArray.count; } From c2c53fab15bc0fcc27a9bfad50a168efab92787f 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 a28236c04..53415b066 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 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { 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 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [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 3340f3bd72c98cbe2efc0a348ac2835e55fa918c 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 53415b066..0e2f14ea3 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 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou NSInteger shrinkedSectionsBitMask; - DirectorySectionHeaderContainerView *directorySectionContainer; - UILabel *networkLabel; - UILabel *directoryServerLabel; - NSMutableDictionary *roomTagsListenerByUserId; // Timer to not refresh publicRoomsDirectoryDataSource on every keystroke. From 19d25381d53377fb0b9880cf32538e94843ccb6b 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 | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index b3c820b73..979b30610 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -58,8 +58,6 @@ plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"rooms_floating_action"] target:self action:@selector(onPlusButtonPressed)]; - - self.enableStickyHeaders = YES; } - (void)viewWillAppear:(BOOL)animated @@ -96,19 +94,19 @@ [super refreshCurrentSelectedCell:forceVisible]; } -- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section -{ - CGRect frame = [tableView rectForHeaderInSection:section]; - frame.size.height = self.stickyHeaderHeight; - - return [recentsDataSource viewForHeaderInSection:section withFrame:frame]; -} - - (void)onPlusButtonPressed { [self showRoomDirectory]; } +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + // Hide the header to merge Invites and Rooms into a single list. + return 0.0; +} + #pragma mark - - (void)scrollToNextRoomWithMissedNotifications From fb57e1bd04bd8b4ebd69eef43582e52253afbf8c 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 486c751b3..f84e39fca 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 a93359f49d0de59d80de3b7833770c00cf939f6a 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 d9fa91f5a..04a669524 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 f11645924..75c7c7fe2 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 e1524bbc8..868341391 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 52afbc691..d58cacb4b 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 234022cf2..5585055fb 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 8a4575e64..bf7636ecb 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 c14cdda6b..216722a92 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 32ede1f7f..904fed413 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 38a6e4d34..9278d4a1d 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 f78010dfe..6e68a7a94 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 718f7de9b..0d516eb93 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 f6fcd78c3..bd4577fda 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 b6a2f0829..415960764 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 a3d3a68e7..63fb432a9 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 812f4def9..1fa265a06 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 c47bf5f51..1a10b6f27 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 4b144ba88..9c3048ec6 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 9e57955c2..9fc3c92a9 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 9a9f7396b..a253b4267 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 4b717f3f8..0ad6dbef6 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 0794ad097..cac4cc986 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 14a8abd72..1bbec2ef6 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 9b2d328c4..88fc75f85 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 43a665081..da074b767 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 685722293..de6fbf1ab 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 ba05c0233..1cfa73d7f 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 6cb2574879e4e6bb58254850384f4ed04b183999 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 79cbf86e7..e2e839d99 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -74,8 +74,6 @@ // 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 @@ - (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 @@ #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 4f16c3f8b506e3db61d2fce13d7db8fb17bda829 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 3cfcf65dc..76cf11fd0 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 be5ad0b46..049b830e7 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -4210,7 +4210,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Show encrypted message notification content by default. if (!RiotSettings.shared.isShowDecryptedContentInNotificationsHasBeenSetOnce) { - RiotSettings.shared.showDecryptedContentInNotifications = YES; + RiotSettings.shared.showDecryptedContentInNotifications = BuildSettings.decryptNotificationsByDefault; } } From 5b581fccc0499754e1e665bf4b45e51f3ba93658 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 21dd49f04..bfc1545db 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 2edca4032..3c88e866c 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 eef3e9aa3..b615b72d6 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 57f016874..2f9ab124a 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 a574b8bb5..3f2f5f793 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 c749bcd47..36b2e5248 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 9a37e9917..d29dbeebb 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 694294566d6c4c272ec614cc6a9ce0842685db10 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 3f2f5f793..03d5140b0 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 02da89dda014bfce5204b73b6322c285fd8ba844 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 6387f123d..468d42cce 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 3f9d9b1bd3cfdcf3008778cf104c625a35661c4f 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 3338f979b..b6367ad50 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5831,7 +5831,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; 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 03d5140b0..dd641929b 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 68373f9f61c10a470f331ae79afb78532fea6663 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 dd641929b..4be0e2c19 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 7d455c0a3..f6b558af8 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 57aa4bba8..443dfa7f7 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 aea36c1886455ce32113ca4a3c6c3cae00cc4070 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 c0abee843..9a72c0902 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 4f6f59f11a666b2996e50cbc95c9d85d4d096069 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 d15a0c990..7890fd3a0 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 da5edee222f829f4c54d7ae0cf3f8c82894d6d23 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 68373f9f61c10a470f331ae79afb78532fea6663. --- .../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 4be0e2c19..dd641929b 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 f6b558af8..7d455c0a3 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 443dfa7f7..57aa4bba8 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 fa69b983f8df5af834c70d5a319d9dbfa18078e7 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 9ffad8128..30e86e015 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 7a6663faf..28e6e04dc 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6188,9 +6188,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]; } -- (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 dd641929b..d24be6379 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 b7c903da745f984b5f35bca194fd0bcad2e475cc 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 da5edee222f829f4c54d7ae0cf3f8c82894d6d23. --- .../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 d24be6379..c044c96c2 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 7d455c0a3..f6b558af8 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 57aa4bba8..443dfa7f7 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 2bfd490241e3cc25b1f8e81bfe27f8eeef007e89 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 31e7491fe..3548311ee 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 b70c5b02c69a16005efb5ec3285cb5b3190dfd88 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 | 43 +++++++++---- .../VoiceMessagePlaybackController.swift | 60 ++++++++++++++----- 5 files changed, 111 insertions(+), 48 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 7890fd3a0..127797926 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 76f5dfbfc..4da9f9779 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 c044c96c2..504d96da7 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 f6b558af8..82a16c977 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,14 +75,6 @@ import Foundation // 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 @@ -76,5 +82,18 @@ import Foundation audioRecoder.stopRecording() } + + guard let audioPlayersEnumerator = audioPlayers.objectEnumerator() else { + return + } + + for case let audioPlayer as VoiceMessageAudioPlayer in audioPlayersEnumerator { + if audioPlayer === service { + continue + } + + audioPlayer.stop() + audioPlayer.unloadContent() + } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 443dfa7f7..d3fc05e74 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 } - self.loading = false - self.audioPlayer.loadContentFromURL(result.1) - self.duration = result.2 - self.samples = result.3 + // Avoid listening to old audio player delegates if the attachment for this playbackController/cell changes + self.audioPlayer?.deregisterDelegate(self) - self.updateUI() + self.audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) + self.audioPlayer?.registerDelegate(self) + + self.loading = false + self.urlToLoad = result.url + self.duration = result.duration + self.samples = result.samples + + 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 d4212d07a5cbf7c77156ae39bc33a33ff1864a20 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 000000000..c6b229539 --- /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 000000000..b30282798 --- /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 000000000..5a372c146 --- /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 822eac15193d866ac41780b8d235bfea1d349d6d 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 bc28f2c30..66a9ca1c7 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 87d62650673cf6a103b663f4dea5d478e7cffed9 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 4da9f9779..1fe928302 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 504d96da7..d6e330fa4 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 d3fc05e74..c2f2a18c2 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 36b2e5248..4492b671f 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 1431d8fd65421af3081950c66e4cdff2aa252fa0 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 3e9e33f5c..d1cd1b3a9 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 1cdf7da187c3cb81f5d1654cb0d4859bff688a18 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 000000000..eac0c885d --- /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 b0c48ecee64bd71e01a19617bf6f2a9bae5d7f96 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 127797926..ece5b8408 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 c2f2a18c2..9abc99e3b 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 4492b671f..e3f53a434 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 d29dbeebb..3cfaebff7 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 72522dae676b111683d9a4ad0353282f8813f6d4 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 ece5b8408..612de8a21 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 df1a09260c049444120ef0edbad54be165a7c371 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 28e6e04dc..99c198171 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6190,7 +6190,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (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 d6e330fa4..ea78372ff 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 8225b800e3e67f594379a6fa53f5972b61870770 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 72522dae676b111683d9a4ad0353282f8813f6d4. --- .../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 612de8a21..ece5b8408 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 d47b0a2289d37d45401b897e8c705923fab93e0f 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 c3c50db54..5772467ee 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 5d1e6b212..4c799b26f 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 6a35e1c96e4017fa57582b04d0bce14f74a73601 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 8225b800e3e67f594379a6fa53f5972b61870770. --- .../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 ece5b8408..612de8a21 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 951b0aa23b269bc828e214016eb3796747586436 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 612de8a21..73d4df484 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 c0067a81b32390f301abc15a54a660e5b0d81bdd 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 30e86e015..9ffad8128 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 87267e923..3dbca7ac7 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 6dfc76dd9..19e61aed8 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 533702c2b..f6260d648 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 e1223b4c7..2234cbf9a 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -82,7 +82,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; - (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView { - if (BuildSettings.voiceMessagesEnabled == NO) { + if (RiotSettings.shared.enableVoiceMessages == NO) { return; } @@ -407,7 +407,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; [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 @@ const CGFloat kComposerContainerTrailingPadding = 12; { 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 a6dad221f..92cfd7e61 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -142,7 +142,8 @@ enum 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 @@ TableViewSectionsDelegate> { 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 @@ TableViewSectionsDelegate> [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 @@ TableViewSectionsDelegate> } } +- (void)toggleEnableVoiceMessages:(UISwitch *)sender +{ + if (sender) + { + RiotSettings.shared.enableVoiceMessages = sender.isOn; + + [self.tableView reloadData]; + } +} + - (void)togglePinRoomsWithMissedNotif:(id)sender { UISwitch *switchButton = (UISwitch*)sender; From 1b042db4d326c9c184ef1e4c83abbb62079dd7dc 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 1fe928302..616ed1d34 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 0ea7a5594..fafabd79a 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 ea78372ff..f6dc56577 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 82a16c977..7def8d7e1 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 9abc99e3b..8fcbc55e4 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 e3f53a434..443168e2f 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 3cfaebff7..4650d6fae 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 9a72c0902..8685109c4 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 92cfd7e61..11bbcb3d8 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2802,7 +2802,7 @@ TableViewSectionsDelegate> } } -- (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 @@ TableViewSectionsDelegate> [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 @@ TableViewSectionsDelegate> [[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 @@ TableViewSectionsDelegate> } } -- (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 @@ TableViewSectionsDelegate> } } -- (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 @@ TableViewSectionsDelegate> __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 @@ TableViewSectionsDelegate> [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 @@ TableViewSectionsDelegate> 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 d7a715467e8cbe12e6e03bfb9eaf317b98e1a3e2 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 616ed1d34..c4bb0880e 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 fafabd79a..76043f073 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 8a5390154ea0b6bce795537d1e185a90270fb895 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 be5868916..6920f8087 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 495d3468b..569803fbf 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 44a46cbd0..48dc70d3e 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 ff2e04626..092a3b874 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 fbd8a25f0..d4f7c05b3 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3936,13 +3936,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [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 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.roomInfoCoordinatorBridgePresenter = nil; } +- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didRequestMentionForMember:(MXRoomMember *)member +{ + [self mention:member]; +} + #pragma mark - RemoveJitsiWidgetViewDelegate - (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view From a25a50e60e88d029e562536e1aa07e0942893bb3 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 36f5b61169ce10ecb2385ba044d8716e1416f4cd 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 7bb3eb350..99bedb824 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 a5e2e60c9..1628e83d8 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 dd2aaa904b0c424263a6b43018256ec40b08d1a1 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 b2d0ed3c5..8a8b129b9 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 0600000bc4702077b8cc4a14b61ad6b0a741f2ed 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 5412eb511..4a0551196 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 873054bb35f440c645ede57ad76c3fe1b493f5ca 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 5412eb511..9ed3a5e10 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 c27b0be4fa0f7af8731f934eb7198a9dfd1e3256 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 9ed3a5e10..05a4a4eec 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 73dedc211fdf5ee9ee1d6e21211f1fc8907805ac 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 0e2f14ea3..123d9da69 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -71,17 +71,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou 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 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou 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 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // 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 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #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 8b03bb671f907030beda214c442dd63693b2e73b 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 49dae406a..7001f1822 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 9f6663f20ce08aabfdc32a2eec1494dfe7c30ebf 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 be091fc8c..e0140c153 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 34727fb122a06ac05426bddfa9080281b8a4c8a7 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 7001f1822..81bdbedb3 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 07eec9bfb6befdcd4c77598a69df3b2593a48732 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 ab35e9993..b33325642 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 @@ -