From 9448a6fc15e4d8223147ef70a6e2063b0900465e Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Wed, 2 Jun 2021 07:36:32 +0200 Subject: [PATCH 01/96] 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 02/96] #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 03/96] #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 04/96] #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 05/96] #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 06/96] #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 07/96] #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 08/96] #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 09/96] #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 13/96] #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 14/96] #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 17/96] #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 18/96] #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 19/96] #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 20/96] #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 21/96] #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 22/96] #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 23/96] #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 24/96] #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 25/96] #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 26/96] #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 27/96] #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 28/96] #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 29/96] #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 30/96] #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 31/96] #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 32/96] #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 1e5d264288ffe245cb0b0b99826d6462049246e3 Mon Sep 17 00:00:00 2001 From: jelv Date: Fri, 2 Jul 2021 10:52:46 +0000 Subject: [PATCH 33/96] Translated using Weblate (Dutch) Currently translated at 100.0% (1233 of 1233 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ --- Riot/Assets/nl.lproj/Vector.strings | 90 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 4b144ba88..7db13f3b6 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -611,7 +611,7 @@ "room_does_not_exist" = "%@ bestaat niet"; // Key backup wrong version "e2e_key_backup_wrong_version_title" = "Nieuwe sleutelback-up"; -"e2e_key_backup_wrong_version" = "Er is een nieuwe sleutelback-up voor versleutelde berichten gedetecteerd.\n\nIndien deze niet van u komt, stel dan een nieuw wachtwoord in in de instellingen."; +"e2e_key_backup_wrong_version" = "Er is een nieuwe sleutelback-up voor versleutelde berichten gedetecteerd.\n\nIndien deze niet van u komt, stel dan een nieuw veiligheidswachtwoord in in de instellingen."; "e2e_key_backup_wrong_version_button_settings" = "Instellingen"; "e2e_key_backup_wrong_version_button_wasme" = "Ik was het"; "key_backup_setup_title" = "Sleutelback-up"; @@ -623,10 +623,10 @@ "key_backup_setup_intro_setup_action_without_existing_backup" = "Begin sleutelback-up te gebruiken"; "key_backup_setup_intro_manual_export_info" = "(Geavanceerd)"; "key_backup_setup_intro_manual_export_action" = "Sleutels handmatig exporteren"; -"key_backup_setup_passphrase_title" = "Beveilig uw back-up met een wachtwoord"; -"key_backup_setup_passphrase_info" = "We bewaren een versleutelde kopie van uw sleutels op onze server. Bescherm uw back-up met een wachtwoord om deze veilig te houden.\n\nVoor maximale beveiliging zou dit moeten verschillen van uw accountwachtwoord."; +"key_backup_setup_passphrase_title" = "Beveilig uw back-up met een veiligheidswachtwoord"; +"key_backup_setup_passphrase_info" = "We bewaren een versleutelde kopie van uw sleutels op onze server. Bescherm uw back-up met een veiligheidswachtwoord om deze veilig te houden.\n\nVoor maximale beveiliging zou dit moeten verschillen van uw accountwachtwoord."; "key_backup_setup_passphrase_passphrase_title" = "Invoeren"; -"key_backup_setup_passphrase_passphrase_placeholder" = "Voer wachtwoord in"; +"key_backup_setup_passphrase_passphrase_placeholder" = "Wachtwoord invoeren"; "key_backup_setup_passphrase_passphrase_valid" = "Top!"; "key_backup_setup_passphrase_passphrase_invalid" = "Probeer nog een woord toe te voegen"; "key_backup_setup_passphrase_confirm_passphrase_title" = "Bevestigen"; @@ -634,35 +634,35 @@ "key_backup_setup_passphrase_confirm_passphrase_valid" = "Top!"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "Wachtwoorden komen niet overeen"; "key_backup_setup_passphrase_set_passphrase_action" = "Wachtwoord instellen"; -"key_backup_setup_passphrase_setup_recovery_key_info" = "Of beveilig uw back-up met een herstelsleutel, en bewaar deze op een veilige plaats."; -"key_backup_setup_passphrase_setup_recovery_key_action" = "(Geavanceerd) Instellen met herstelsleutel"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Of beveilig uw back-up met een veiligheidssleutel en bewaar deze op een veilige plaats."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(Geavanceerd) Instellen met veiligheidssleutel"; "key_backup_setup_success_title" = "Klaar!"; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "Er wordt een back-up van uw sleutels gemaakt.\n\nUw herstelsleutel is een veiligheidsnet - u kunt deze gebruiken om de toegang tot uw versleutelde berichten te herstellen als u uw wachtwoord zou vergeten.\n\nBewaar uw herstelsleutel op een heel veilig plaats, zoals een wachtwoordbeheerder (of een kluis)."; -"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Herstelsleutel opslaan"; +"key_backup_setup_success_from_passphrase_info" = "Er wordt een back-up van uw sleutels gemaakt.\n\nUw veiligheidssleutel is een veiligheidsnet - u kunt deze gebruiken om de toegang tot uw versleutelde berichten te herstellen als u uw wachtwoord zou vergeten.\n\nBewaar uw veiligheidssleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)."; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Veiligheidssleutel opslaan"; "key_backup_setup_success_from_passphrase_done_action" = "Klaar"; // Success from recovery key -"key_backup_setup_success_from_recovery_key_info" = "Er wordt een back-up van uw sleutels gemaakt.\n\nMaak een kopie van deze herstelsleutel en bewaar deze op een veilige plaats."; -"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Herstelsleutel"; +"key_backup_setup_success_from_recovery_key_info" = "Er wordt een back-up van uw sleutels gemaakt.\n\nMaak een kopie van deze veiligheidssleutel en bewaar deze op een veilige plaats."; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Veiligheidssleutel"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "Maak een kopie"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "Ik heb een kopie gemaakt"; "key_backup_recover_title" = "Versleutelde berichten"; -"key_backup_recover_invalid_passphrase_title" = "Onjuist herstelwachtwoord"; -"key_backup_recover_invalid_passphrase" = "De back-up kon niet ontsleuteld worden met dit wachtwoord: controleer of u het herstelwachtwoord juist hebt ingevoerd."; -"key_backup_recover_invalid_recovery_key_title" = "Herstelsleutel komt niet overeen"; -"key_backup_recover_invalid_recovery_key" = "De back-up kon niet ontsleuteld worden met deze sleutel: controleer of u de juiste herstelsleutel hebt ingevoerd."; -"key_backup_recover_from_passphrase_info" = "Gebruik uw herstelwachtwoord om uw versleutelde berichtgeschiedenis te ontgrendelen"; +"key_backup_recover_invalid_passphrase_title" = "Onjuist veiligheidswachtwoord"; +"key_backup_recover_invalid_passphrase" = "De back-up kon niet ontsleuteld worden met dit wachtwoord: controleer of u het veiligheidswachtwoord juist hebt ingevoerd."; +"key_backup_recover_invalid_recovery_key_title" = "Veiligheidssleutel komt niet overeen"; +"key_backup_recover_invalid_recovery_key" = "De back-up kon niet ontsleuteld worden met deze sleutel: controleer of u de juiste veiligheidssleutel hebt ingevoerd."; +"key_backup_recover_from_passphrase_info" = "Gebruik uw veiligheidswachtwoord om uw versleutelde berichtengeschiedenis te ontgrendelen"; "key_backup_recover_from_passphrase_passphrase_title" = "Invoeren"; -"key_backup_recover_from_passphrase_passphrase_placeholder" = "Voer wachtwoord in"; +"key_backup_recover_from_passphrase_passphrase_placeholder" = "Wachtwoord invoeren"; "key_backup_recover_from_passphrase_recover_action" = "Geschiedenis ontgrendelen"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Herstelwachtwoord vergeten? Dan kunt u "; -"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "uw herstelsleutel gebruiken"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Veiligheidswachtwoord vergeten? Dan kunt u "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "uw veiligheidssleutel gebruiken"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; -"key_backup_recover_from_recovery_key_info" = "Gebruik uw herstelsleutel om uw versleutelde berichtgeschiedenis te ontgrendelen"; +"key_backup_recover_from_recovery_key_info" = "Gebruik uw veiligheidssleutel om uw versleutelde berichtengeschiedenis te ontgrendelen"; "key_backup_recover_from_recovery_key_recovery_key_title" = "Invoeren"; -"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Voer herstelsleutel in"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Veiligheidssleutel invoeren"; "key_backup_recover_from_recovery_key_recover_action" = "Geschiedenis ontgrendelen"; -"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Herstelsleutel verloren? U kunt er een nieuwe aanmaken in de instellingen."; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Veiligheidssleutel verloren? U kunt er een nieuwe aanmaken in de instellingen."; "key_backup_recover_success_info" = "Back-up hersteld!"; "key_backup_recover_done_action" = "Klaar"; "key_backup_setup_banner_title" = "Verlies nooit uw versleutelde berichten"; @@ -913,7 +913,7 @@ // MARK: - Favourites "favourites_empty_view_title" = "Favoriete gesprekken en personen"; -"home_empty_view_information" = "De alles-in-één veilige chat app voor teams, vrienden en organisaties. Klik op de onderstaande + knop om gesprekken te starten met personen en groepen."; +"home_empty_view_information" = "De alles-in-één veilige chat-app voor teams, vrienden en organisaties. Druk op de + knop hieronder om personen en gesprekken toe te voegen."; // MARK: - Home @@ -1032,32 +1032,32 @@ "secrets_setup_recovery_key_done_action" = "Klaar"; "secrets_setup_recovery_key_export_action" = "Opslaan"; "secrets_setup_recovery_key_loading" = "Laden…"; -"secrets_setup_recovery_key_information" = "Bewaar uw Herstelsleutel op een veilige plek. Het kan gebruikt worden voor het ontgrendelen van uw versleutelde berichten en data."; -"secrets_recovery_with_key_invalid_recovery_key_message" = "Verifieer dat u de juiste herstelsleutel heeft ingevoerd."; +"secrets_setup_recovery_key_information" = "Bewaar uw veiligheidssleutel op een veilige plek. Deze kan gebruikt worden om uw versleutelde berichten en data te ontsleutelen."; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Verifieer dat u de juiste veiligheidssleutel heeft ingevoerd."; "secrets_recovery_with_key_invalid_recovery_key_title" = "Geen toegang tot geheime opslag"; "secrets_recovery_with_key_recover_action" = "Gebruik sleutel"; -"secrets_recovery_with_key_recovery_key_placeholder" = "Voer de herstelsleutel in"; +"secrets_recovery_with_key_recovery_key_placeholder" = "Veiligheidssleutel invoeren"; "secrets_recovery_with_key_recovery_key_title" = "Invoeren"; -"secrets_recovery_with_key_information_verify_device" = "Gebruik uw herstelsleutel om dit apparaat te verifiëren."; -"secrets_recovery_with_key_information_default" = "Ontvang toegang tot uw versleutelde berichtengeschiedenis en uw kruislings ondertekenen ID voor het verifiëren van andere sessie door het invoeren van uw Herstelsleutel."; +"secrets_recovery_with_key_information_verify_device" = "Gebruik uw veiligheidssleutel om dit apparaat te verifiëren."; +"secrets_recovery_with_key_information_default" = "Ontvang toegang tot uw versleutelde berichtengeschiedenis en kruislings ondertekenen voor het verifiëren van andere sessie door het invoeren van uw veiligheidssleutel."; // Recover with key -"secrets_recovery_with_key_title" = "Herstelsleutel"; -"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Verifieer dat u het juiste Herstelwachtwoord heeft ingevoerd."; +"secrets_recovery_with_key_title" = "Veiligheidssleutel"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Verifieer dat u het juiste veiligheidswachtwoord heeft ingevoerd."; "secrets_recovery_with_passphrase_invalid_passphrase_title" = "Geen toegang tot geheime opslag"; "secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; -"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "uw Herstelsleutel gebruiken"; -"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Herstelwachtwoord vergeten? Dan kunt u "; -"secrets_recovery_with_passphrase_recover_action" = "Gebruik Wachtwoord"; -"secrets_recovery_with_passphrase_passphrase_placeholder" = "Voer uw Herstelwachtwoord in"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "uw veiligheidssleutel gebruiken"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Veiligheidswachtwoord vergeten? Dan kunt u "; +"secrets_recovery_with_passphrase_recover_action" = "Gebruik wachtwoord"; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Voer uw veiligheidswachtwoord in"; "secrets_recovery_with_passphrase_passphrase_title" = "Invoeren"; -"secrets_recovery_with_passphrase_information_verify_device" = "Gebruik uw Herstelwachtwoord om dit apparaat te verifiëren."; -"secrets_recovery_with_passphrase_information_default" = "Ontvang toegang tot uw versleutelde berichtengeschiedenis en uw kruislings ondertekenen ID voor het verifiëren van andere sessies door het invoeren van uw Herstelwachtwoord."; +"secrets_recovery_with_passphrase_information_verify_device" = "Gebruik uw veiligheidswachtwoord om dit apparaat te verifiëren."; +"secrets_recovery_with_passphrase_information_default" = "Ontvang toegang tot uw versleutelde berichtengeschiedenis en kruislings ondertekenen voor het verifiëren van andere sessies door het invoeren van uw veiligheidswachtwoord."; // Recover with passphrase -"secrets_recovery_with_passphrase_title" = "Herstelwachtwoord"; +"secrets_recovery_with_passphrase_title" = "Veiligheidswachtwoord"; "secrets_recovery_reset_action_part_2" = "Alles opnieuw instellen"; // MARK: - Secrets recovery @@ -1067,7 +1067,7 @@ "user_verification_session_details_verify_action_current_user_manually" = "Handmatig middels een tekst"; "user_verification_session_details_verify_action_current_user" = "Interactief Verifiëren"; "user_verification_session_details_additional_information_untrusted_current_user" = "Als u zich niet heeft aangemeld bij deze sessie, is uw account wellicht geschonden."; -"user_verification_session_details_additional_information_untrusted_other_user" = "Totdat deze persoon de sessie vertrouwd zijn berichten gelabeld met een waarschuwing. Een alternatief is handmatig verifiëren."; +"user_verification_session_details_additional_information_untrusted_other_user" = "Totdat deze persoon deze sessie vertrouwd, zijn berichten gelabeld met waarschuwingen. Een andere mogelijkheid is om de persoon handmatig te verifiëren."; "user_verification_session_details_information_untrusted_other_user" = " heeft zich in een nieuwe sessie aangemeld:"; "user_verification_session_details_information_untrusted_current_user" = "Verifieer deze sessie om het als vertrouwd te markeren en het toegang te geven tot versleutelde berichten:"; "user_verification_session_details_information_trusted_other_user_part2" = " verifieer het:"; @@ -1180,8 +1180,8 @@ "key_verification_verify_sas_title_emoji" = "Vergelijk de emoji's"; "device_verification_self_verify_wait_recover_secrets_checking_availability" = "Controleren op andere verificatie mogelijkheden..."; "device_verification_self_verify_wait_recover_secrets_additional_information" = "Wanneer u geen toegang meer heeft tot een bestaande sessie"; -"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Uw Herstelwachtwoord of -sleutel gebruiken"; -"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Herstelsleutel gebruiken"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Uw veiligheidswachtwoord of -sleutel gebruiken"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Veiligheidssleutel gebruiken"; "device_verification_self_verify_wait_additional_information" = "Dit werkt met Element en andere Matrix-apps die kruislings ondertekenen ondersteunen."; "device_verification_self_verify_wait_information" = "Verifieer deze sessie vanaf een van uw andere sessies, om toegang te krijgen tot de versleutelde berichten.\n\nGebruik de laatste versie van Element op uw andere apparaten:"; "device_verification_self_verify_wait_new_sign_in_title" = "Verifieer deze login"; @@ -1292,11 +1292,11 @@ "room_details_advanced_e2e_encryption_disabled_for_dm" = "Versleuteling is hier niet ingeschakeld."; "room_details_advanced_e2e_encryption_enabled_for_dm" = "Versleuteling is hier ingeschakeld"; "room_details_advanced_room_id_for_dm" = "ID:"; -"room_details_no_local_addresses_for_dm" = "Dit heeft geen lokaaladres"; +"room_details_no_local_addresses_for_dm" = "Geen lokaaladres bekend"; "room_details_access_section_directory_toggle_for_dm" = "Weergeven in publieke groepsgesprekkencatalogus"; "room_details_access_section_anyone_for_dm" = "Iedereen die de koppeling kent, inclusief gasten"; "room_details_access_section_anyone_apart_from_guest_for_dm" = "Iedereen die de koppeling kent, behalve gasten"; -"room_details_access_section_for_dm" = "Wie mag toegang hebben?"; +"room_details_access_section_for_dm" = "Wie heeft toegang?"; "room_details_room_name_for_dm" = "Naam"; "room_details_photo_for_dm" = "Foto"; "room_details_title_for_dm" = "Details"; @@ -1343,17 +1343,17 @@ "security_settings_cryptography" = "CRYPTOGRAFIE"; "security_settings_crosssigning_complete_security" = "Beveiliging afronden"; "security_settings_crosssigning_reset" = "Reset"; -"security_settings_crosssigning_bootstrap" = "Stel in"; -"security_settings_crosssigning_info_ok" = "Cross-signing is klaar voor gebruik."; +"security_settings_crosssigning_bootstrap" = "Instellen"; +"security_settings_crosssigning_info_ok" = "Kruiselings ondertekenen is klaar voor gebruik."; "security_settings_crosssigning_info_trusted" = "Kruislings ondertekenen is ingeschakeld. U kunt andere personen en sessies verifiëren met kruislings ondertekenen, maar u kunt dit nog niet vanaf deze sessie doordat de versleutelingssleutel ontbreekt. Rond de beveiliging van deze sessie af."; "security_settings_crosssigning_info_exists" = "Uw account heeft een kruislings ondertekenen ID, maar is nog niet geverifieerd door deze sessie. Rond de beveiliging van deze sessie af."; "security_settings_crosssigning_info_not_bootstrapped" = "Kruislings ondertekenen is nog niet ingesteld."; "security_settings_crosssigning" = "KRUISLINGS ONDERTEKENEN"; "security_settings_backup" = "BERICHTENBACK-UP"; -"security_settings_secure_backup_delete" = "Verwijder backup"; +"security_settings_secure_backup_delete" = "Back-up verwijderen"; "security_settings_secure_backup_synchronise" = "Synchroniseren"; "security_settings_secure_backup_setup" = "Instellen"; -"security_settings_secure_backup_description" = "Waarborg uw toegang tot uw versleutelde berichten & data door de versleutelingssleutels op te slaan. Uw sleutels zullen worden beveiligd met een unieke beveiligingssleutel."; +"security_settings_secure_backup_description" = "Maak een back-up van uw versleutelingssleutel bij uw account data voor het geval u toegang verliest tot uw sessies. Uw sleutels zullen worden beveiligd met een unieke veiligheidssleutel."; "security_settings_secure_backup" = "VEILIGE BACK-UP"; "security_settings_crypto_sessions_description_2" = "Als u deze inlog niet herkent, verander uw wachtwoord en reset uw Veilige Back-up."; "security_settings_crypto_sessions_loading" = "Sessies laden…"; From 7824de6647bea0b256c1a85c10041b41c244bca1 Mon Sep 17 00:00:00 2001 From: Tim Al Date: Fri, 2 Jul 2021 10:50:26 +0000 Subject: [PATCH 34/96] Translated using Weblate (Dutch) Currently translated at 100.0% (1233 of 1233 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ --- Riot/Assets/nl.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 7db13f3b6..1b5064d3e 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1065,7 +1065,7 @@ "secrets_recovery_reset_action_part_1" = "Alle herstelopties vergeten of verloren? "; "user_verification_session_details_verify_action_other_user" = "Handmatig verifiëren"; "user_verification_session_details_verify_action_current_user_manually" = "Handmatig middels een tekst"; -"user_verification_session_details_verify_action_current_user" = "Interactief Verifiëren"; +"user_verification_session_details_verify_action_current_user" = "Interactief verifiëren"; "user_verification_session_details_additional_information_untrusted_current_user" = "Als u zich niet heeft aangemeld bij deze sessie, is uw account wellicht geschonden."; "user_verification_session_details_additional_information_untrusted_other_user" = "Totdat deze persoon deze sessie vertrouwd, zijn berichten gelabeld met waarschuwingen. Een andere mogelijkheid is om de persoon handmatig te verifiëren."; "user_verification_session_details_information_untrusted_other_user" = " heeft zich in een nieuwe sessie aangemeld:"; @@ -1266,7 +1266,7 @@ // Room widget permissions "room_widget_permission_title" = "Widget laden"; "widget_picker_manage_integrations" = "Beheer integraties…"; -"widget_integration_manager_disabled" = "U moet een integratebeheerder inschakelen in uw instellingen"; +"widget_integration_manager_disabled" = "U moet integratiebeheer inschakelen in de instellingen"; "widget_menu_remove" = "Verwijderen voor iedereen"; "widget_menu_revoke_permission" = "Toegang intrekken voor mij"; "widget_menu_open_outside" = "Openen in browser"; From 9b7357e0e17641a6980205884ed2ff3114503ba0 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 5 Jul 2021 11:59:35 +0100 Subject: [PATCH 35/96] Remove background colour from RoomsLiveViewController. --- .../Modules/Share/Listing/RoomsListViewController.xib | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib index 630d940a3..bc77d3275 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib +++ b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.xib @@ -1,11 +1,9 @@ - - - - + + - + @@ -39,7 +37,6 @@ - @@ -49,6 +46,7 @@ + From b24879ac6691b578fb286a17c931ee3b27ae96a8 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Jul 2021 08:57:51 +0200 Subject: [PATCH 36/96] 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 37/96] 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 38/96] 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 39/96] 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 113fd85103f80ae222760e517fec12af2d04e44e Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 6 Jul 2021 23:03:56 +0200 Subject: [PATCH 40/96] #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 6f072f4a54990da7eafca24998910e6fab78ab88 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 7 Jul 2021 17:44:24 +0300 Subject: [PATCH 41/96] Update app version to 1.4.5 --- Config/AppIdentifiers.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index a56c9e00a..391931749 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.4 -CURRENT_PROJECT_VERSION = 1.4.4 +MARKETING_VERSION = 1.4.5 +CURRENT_PROJECT_VERSION = 1.4.5 // Team From 6e55086c8774e021f7ce72ce2eb4dbcc2400bbea Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 7 Jul 2021 17:44:36 +0300 Subject: [PATCH 42/96] Update CHANGES.rst --- CHANGES.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 29c0a6b44..65bed365e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,24 @@ +Changes in 1.4.5 (2021-07-07) +================================================= + +✨ Features + * + +🙌 Improvements + * + +🐛 Bugfix + * Notifications: Fix an issue where the app is unresponsive after getting some notifications (#4534). + +⚠️ API Changes + * + +🗣 Translations + * + +🧱 Build + * + Changes in 1.4.4 (2021-06-30) ================================================= From e13230992de980f9bad7c2c175847c02dd037c35 Mon Sep 17 00:00:00 2001 From: HelaBasa Date: Thu, 8 Jul 2021 03:34:48 +0000 Subject: [PATCH 43/96] Translated using Weblate (Sinhala) Currently translated at 17.1% (6 of 35 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/si/ --- Riot/Assets/si.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/si.lproj/Localizable.strings b/Riot/Assets/si.lproj/Localizable.strings index 432026982..ac8956825 100644 --- a/Riot/Assets/si.lproj/Localizable.strings +++ b/Riot/Assets/si.lproj/Localizable.strings @@ -17,3 +17,6 @@ /* New message from a specific person, not referencing a room. Content included. */ "MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (සමූහ ඇමතුම)"; From 3aac4af63d86daa05bf2b0c896e49d5f74d95f83 Mon Sep 17 00:00:00 2001 From: Deleted User Date: Thu, 8 Jul 2021 13:16:07 +0000 Subject: [PATCH 44/96] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (35 of 35 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/nb_NO/ --- Riot/Assets/nb-NO.lproj/Localizable.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/nb-NO.lproj/Localizable.strings b/Riot/Assets/nb-NO.lproj/Localizable.strings index 26102b751..5f6a77573 100644 --- a/Riot/Assets/nb-NO.lproj/Localizable.strings +++ b/Riot/Assets/nb-NO.lproj/Localizable.strings @@ -111,3 +111,9 @@ "MSG_FROM_USER" = "%@ sendte en melding"; /* Message title for a specific person in a named room */ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@ i %@"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (Gruppeanrop)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Gruppeanrop startet"; From 07036389b3146bc5ec7e0dc4cdd180394743e13d Mon Sep 17 00:00:00 2001 From: Deleted User Date: Thu, 8 Jul 2021 13:36:27 +0000 Subject: [PATCH 45/96] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 97.7% (1205 of 1233 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nb_NO/ --- Riot/Assets/nb-NO.lproj/Vector.strings | 89 ++++++++++++++++++++------ 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index c47bf5f51..74f8d9e00 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -479,7 +479,7 @@ "settings_discovery_error_message" = "Det oppstod en feil. Vennligst prøv igjen."; "security_settings_crypto_sessions" = "MINE ØKTER"; "security_settings_secure_backup_setup" = "Sett opp"; -"security_settings_secure_backup_delete" = "Slett"; +"security_settings_secure_backup_delete" = "Slett sikkerhetskopi"; "security_settings_crosssigning_complete_security" = "Komplett sikkerhet"; "security_settings_cryptography" = "KRYPTOGRAFI"; "security_settings_complete_security_alert_title" = "Komplett sikkerhet"; @@ -513,7 +513,7 @@ "key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Bekreft passordfrasen"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "Passordfrasen samsvarer ikke"; "key_backup_setup_passphrase_set_passphrase_action" = "Velg passordfrase"; -"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Gjenopprettingsnøkkel"; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Sikkerhetsnøkkel"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "Lag en kpi"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "Jeg har laget en kopi"; "key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Skriv inn gjenopprettingsnøkkelen"; @@ -834,7 +834,7 @@ "settings_key_backup_info_not_valid" = "Denne økten sikkerhetskopierer ikke dine nøkler, men du har en eksisterende sikkerhetskopi du kan gjenopprette fra og legge til, for å gå videre."; "settings_key_backup_info_valid" = "Denne økten sikkerhetskopierer dine nøkler."; "settings_key_backup_info_version" = "Sikkerhetskopi av nøkler versjon : %@"; -"settings_key_backup_info_signout_warning" = "Før du logger ut, koble denne sesjonen til sikkerhetskopi av nøkler for å unngå tap av nøkler som kanskje bare er lagret på denne enheten."; +"settings_key_backup_info_signout_warning" = "Sikkerhetskopier nøklene dine før du logger av for å unngå å miste dem."; "settings_key_backup_info_none" = "Nøklene dine for denne sesjonen blir ikke sikkerhetskopiert."; "settings_third_party_notices" = "Tredjepartsmerknader"; "settings_labs_e2e_encryption_prompt_message" = "Vennligst logg inn igjen for å ferdigstille oppsett av kryptering."; @@ -864,7 +864,7 @@ "pin_protection_not_allowed_pin" = "Av sikkerhetsårsaker er denne PIN-koden ikke tilgjengelig. Prøv en annen PIN-kode"; "secrets_setup_recovery_passphrase_information" = "Skriv inn en sikkerhetsfrase bare du kjenner, brukes til å sikre hemmeligheter på serveren."; "secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "bruke gjenopprettingsnøkkelen din"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "bruke gjenopprettingsnøkkelen"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "bruk sikkerhetsnøkkelen"; "room_details_access_section_for_dm" = "Hvem har tilgang til dette?"; "identity_server_settings_place_holder" = "Legg inn en identitetsserver"; "identity_server_settings_description" = "Du bruker for øyeblikket %@ for å finne og bli funnet av dine eksisterende kontakter."; @@ -920,13 +920,13 @@ "security_settings_crosssigning" = "KRYSS-SIGNERING"; "security_settings_backup" = "SIKKERHETSKOPI-MELDINGER"; "security_settings_secure_backup_synchronise" = "Synkroniser"; -"security_settings_secure_backup_description" = "Sikre deg mot å miste tilgang til krypterte meldinger og data ved å lagre sikkerhetskopi av krypteringsnøkler på din server."; +"security_settings_secure_backup_description" = "Sikkerhetskopier krypteringsnøklene med kontodataene dine hvis du mister tilgangen til øktene dine. Nøklene dine blir sikret med en unik sikkerhetsnøkkel."; "security_settings_secure_backup" = "SIKKERHETSKOPI"; "security_settings_crosssigning_info_trusted" = "Kryss-signering er aktivert. Du kan stole på andre brukere og dine andre økter basert på kryss-signering, men du kan ikke kryss-signere fra denne økten fordi den ikke har private nøkler for kryss-signering. Fullfør sikkerheten for denne økten."; "security_settings_export_keys_manually" = "Eksporter nøkler manuelt"; -"security_settings_crosssigning_reset" = "Tilbakestill kryss-signering"; -"security_settings_crosssigning_bootstrap" = "Bootstrap kryss-signering"; -"security_settings_crosssigning_info_ok" = "Kryss-signering er aktivert."; +"security_settings_crosssigning_reset" = "Nullstill"; +"security_settings_crosssigning_bootstrap" = "Sett opp"; +"security_settings_crosssigning_info_ok" = "Kryss-signering er klar til bruk."; "security_settings_blacklist_unverified_devices" = "Aldri send meldinger til ikke-klarerte økter"; // AuthenticatedSessionViewControllerFactory @@ -1052,7 +1052,7 @@ "bug_report_prompt" = "Applikasjonen krasjet sist gang. Vil du sende inn en krasj-rapport?"; "public_room_section_title" = "Offentlige rom (på %@):"; "call_no_stun_server_error_message_2" = "Alternativt kan du prøve å bruke den offentlige serveren ved %@, men denne vil være mindre pålitelig, og vil dele din IP-adresse med serveren. Du kan også administrere dette i innstillinger"; -"e2e_key_backup_wrong_version" = "Det har blitt oppdaget en ny sikkerhetskopi av meldingsnøkler .\n\nHvis dette ikke ble intitiert av deg bør du endre passordfrase i innstillinger."; +"e2e_key_backup_wrong_version" = "Det har blitt oppdaget en ny sikkerhetskopi av meldingsnøkler .\n\nHvis dette ikke var deg, angir du en ny sikkerhetsfrase i Innstillinger."; // Key backup wrong version "e2e_key_backup_wrong_version_title" = "Ny sikkerhetskopi av nøkler"; @@ -1136,7 +1136,7 @@ "secure_key_backup_setup_intro_title" = "Sikkert lagringsområde"; "secure_key_backup_setup_intro_use_security_key_info" = "Generer en sikkerhetsnøkkel og lagre den på et trygt sted som i en passordadministrator eller en safe."; "secure_key_backup_setup_intro_use_security_key_title" = "Bruk en sikkerhetsnøkkel"; -"secure_key_backup_setup_intro_use_security_passphrase_title" = "Bruk en passordfrase"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Bruk en sikkerhetsfrase"; "key_backup_setup_intro_manual_export_action" = "Eksporter nøkler manuelt"; "key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Koble denne enheten til sikkerhetskopi av meldingsnøkler"; "key_backup_setup_skip_alert_message" = "Du kan miste dine krypterte meldinger dersom du logger ut eller mister enheten."; @@ -1154,12 +1154,12 @@ "secure_key_backup_setup_existing_backup_error_info" = "Lås den opp for å gjenbruke den på sikkert lagringsområde, eller slett den for å opprette en ny sikkerhetskopi av meldinger på sikkert lagringsområde."; "secure_key_backup_setup_existing_backup_error_title" = "Det finnes allerede en sikkerhetskopi for meldinger"; "secure_key_backup_setup_intro_use_security_passphrase_info" = "Skriv inn en hemmelig frase bare du vet, og generer en nøkkel for sikkerhetskopiering."; -"key_backup_setup_passphrase_setup_recovery_key_info" = "Eller sikre sikkerhetskopien din med en gjenopprettingsnøkkel, og lagre den på et trygt sted."; -"key_backup_setup_passphrase_info" = "Vi lagrer en kryptert kopi av nøklene dine på serveren vår. Beskytt sikkerhetskopien din med en passordfrase for å holde den sikker.\n\nFor maksimal sikkerhet bør dette være forskjellig fra kontopassordet ditt."; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Eller, sikre sikkerhetskopien med en sikkerhetsnøkkel, og lagre den et trygt sted."; +"key_backup_setup_passphrase_info" = "Vi lagrer en kryptert kopi av nøklene dine på serveren vår. Beskytt sikkerhetskopien med en setning for å holde den sikker.\n\nFor maksimal sikkerhet bør dette være forskjellig fra kontopassordet ditt."; // Passphrase -"key_backup_setup_passphrase_title" = "Gjør sikkerhetskopien din sikker med en passordfrase"; +"key_backup_setup_passphrase_title" = "Sikre sikkerhetskopien din med en sikkerhetsfrase"; "key_backup_setup_intro_info" = "Meldinger i krypterte rom er sikret med ende-til-ende-kryptering. Bare du og mottakeren(e) har nøklene til å lese disse meldingene.\n\nLagre nøklene dine på et trygt sted for å unngå å miste dem."; // MARK: Key backup recover @@ -1168,29 +1168,29 @@ // Success from recovery key "key_backup_setup_success_from_recovery_key_info" = "Nøklene dine blir sikkerhetskopiert.\n\nKopier denne gjenopprettingsnøkkelen og lagre den på et trygt sted."; -"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Lagre gjenopprettingsnøkkel"; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Lagre sikkerhetsnøkkel"; // Success from passphrase "key_backup_setup_success_from_passphrase_info" = "Nøklene dine blir sikkerhetskopiert.\n\nGjenopprettingsnøkkelen din er et sikkerhetsnett - du kan bruke den til å gjenopprette tilgangen til de krypterte meldingene dine hvis du glemmer passordfrasen.\n\nLagre gjenopprettingsnøkkelen din på en trygg måte, f.eks. ved hjelp av en passordadministrator (eller i en safe)."; -"key_backup_setup_passphrase_setup_recovery_key_action" = "(Avansert) Sett opp med gjenopprettingsnøkkel"; -"key_backup_recover_invalid_passphrase" = "Sikkerhetskopi kunne ikke dekrypteres med denne passordfrasen: Vennligst sjekk at du har angitt riktig passordfrase."; -"key_backup_recover_invalid_passphrase_title" = "Feil gjenopprettingsfrase"; -"key_backup_recover_invalid_recovery_key" = "Sikkerhetskopi kunne ikke dekrypteres med denne nøkkelen: bekreft at du skrev inn riktig gjenopprettingsnøkkel."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(Avansert) Sett opp med sikkerhetsnøkkel"; +"key_backup_recover_invalid_passphrase" = "Sikkerhetskopiering kunne ikke dekrypteres med denne setningen: bekreft at du har skrevet riktig sikkerhetsfrase."; +"key_backup_recover_invalid_passphrase_title" = "Feil sikkerhetsfrase"; +"key_backup_recover_invalid_recovery_key" = "Sikkerhetskopiering kunne ikke dekrypteres med denne nøkkelen: bekreft at du har angitt riktig sikkerhetsnøkkel."; "key_backup_recover_invalid_recovery_key_title" = "Feil i gjenopprettingsnøkkel"; // Recover from passphrase -"key_backup_recover_from_passphrase_info" = "Bruk gjenopprettingspassordet for å låse opp historikken for dine sikrede meldinger"; +"key_backup_recover_from_passphrase_info" = "Bruk sikkerhetsfrasen for å låse opp den sikre meldingsloggen"; // Recover from private key "key_backup_recover_from_private_key_info" = "Gjenoppretter sikkerhetskopi …"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Kjenner du ikke gjenopprettingspassordet ditt? Du kan "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Kjenner du ikke sikkerhetsfrasen din? Du kan "; "key_backup_recover_from_passphrase_recover_action" = "Lås opp historikk"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "Skriv inn passordfrase"; // Recover from recovery key -"key_backup_recover_from_recovery_key_info" = "Bruk gjenopprettingsnøkkel for å låse opp historikken for sikrede meldinger"; +"key_backup_recover_from_recovery_key_info" = "Bruk sikkerhetsnøkkelen til å låse opp den sikre meldingsloggen"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; "sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Begynn å bruke Sikkert lagringsområde"; "sign_out_non_existing_key_backup_alert_title" = "Du mister tilgangen til de krypterte meldingene dine hvis du logger ut nå"; @@ -1418,3 +1418,50 @@ "room_intro_cell_information_multiple_dm_sentence2" = "Bare dere er i denne samtalen, med mindre noen av dere inviterer andre til å bli med."; "room_intro_cell_information_dm_sentence2" = "Bare dere to er i denne samtalen, ingen andre kan bli med."; "room_intro_cell_information_dm_sentence1_part3" = ". "; +"side_menu_app_version" = "Versjon %@"; +"side_menu_action_feedback" = "Tilbakemelding"; +"side_menu_action_help" = "Hjelp"; +"side_menu_action_settings" = "Innstillinger"; +"side_menu_action_invite_friends" = "Inviter venner"; + +// Mark: - Side menu + +"side_menu_reveal_action_accessibility_label" = "Venstre panel"; +"user_avatar_view_accessibility_hint" = "Endre bruker avatar"; + +// Mark: - User avatar view + +"user_avatar_view_accessibility_label" = "avatar"; +"space_beta_announce_information" = "Plasser er en ny måte å gruppere rom og mennesker på. De er ikke på iOS ennå, men du kan bruke dem nå på nettet og på skrivebordet."; +"space_beta_announce_subtitle" = "Den nye versjonen av lokalsamfunn"; +"space_beta_announce_title" = "Plasser kommer snart"; +"space_beta_announce_badge" = "BETA"; +"space_feature_unavailable_information" = "Plasser er en ny måte å gruppere rom og mennesker på.\n\nDe kommer snart. For nå, hvis du blir med på en annen plattform, vil du kunne få tilgang til alle rom du blir med her."; +"space_feature_unavailable_subtitle" = "Plasser er ikke på iOS ennå, men du kan bruke dem nå på nettet og på skrivebordet"; + +// Mark: - Spaces + +"space_feature_unavailable_title" = "Plasser er ikke her ennå"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Skriv inn sikkerhetsnøkkelen din for å fortsette."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Skriv inn sikkerhetsfrasen for å fortsette."; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "Nøklene dine blir sikkerhetskopiert."; +"event_formatter_group_call_incoming" = "%@ i %@"; +"event_formatter_group_call_leave" = "Forlat"; +"event_formatter_group_call_join" = "Bli med"; +"event_formatter_group_call" = "Gruppeanrop"; +"event_formatter_call_end_call" = "Avslutt samtale"; +"event_formatter_call_retry" = "Prøv på nytt"; +"event_formatter_call_answer" = "Svar"; +"security_settings_secure_backup_restore" = "Gjenopprett fra sikkerhetskopi"; +"security_settings_secure_backup_reset" = "Nullstill"; +"security_settings_secure_backup_info_valid" = "Denne økten tar sikkerhetskopi av nøklene dine."; +"security_settings_secure_backup_info_checking" = "Sjekker…"; +"settings_ui_theme_picker_message_match_system_theme" = "\"Auto\" samsvarer med enhetens systemtema"; +"settings_ui_theme_picker_message_invert_colours" = "\"Auto\" bruker enhetens \"Inverter farger\" innstillinger"; + +// Chat +"room_slide_to_end_group_call" = "Skyv for å avslutte samtalen for alle"; +"room_recents_unknown_room_error_message" = "Finner ikke dette rommet. Forsikre deg om at den eksisterer"; +"room_creation_dm_error" = "Vi kunne ikke opprette DM. Kontroller brukerne du vil invitere, og prøv på nytt."; From d0ebf701818259d38c82dcaf0ab556572c072652 Mon Sep 17 00:00:00 2001 From: HelaBasa Date: Thu, 8 Jul 2021 03:37:54 +0000 Subject: [PATCH 46/96] Translated using Weblate (Sinhala) Currently translated at 1.2% (15 of 1233 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/si/ --- Riot/Assets/si.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/si.lproj/Vector.strings b/Riot/Assets/si.lproj/Vector.strings index 982e41a3b..59e0d02cf 100644 --- a/Riot/Assets/si.lproj/Vector.strings +++ b/Riot/Assets/si.lproj/Vector.strings @@ -1,3 +1,16 @@ // Titles "title_home" = "මුල් පිටුව"; "warning" = "අවවාදයයි"; +"join" = "එක්වන්න"; +"save" = "සුරකින්න"; +"cancel" = "අවලංගු කරන්න"; +"remove" = "ඉවත් කරන්න"; +"leave" = "හැරයන්න"; +"start" = "අරඹන්න"; +"create" = "සාදන්න"; +"continue" = "ඉදිරියට"; +"back" = "ආපසු"; +"next" = "ඊලඟ"; +"title_rooms" = "කාමර"; +"title_people" = "මිනිසුන්"; +"title_favourites" = "ප්‍රියතමයින්"; From 5b581fccc0499754e1e665bf4b45e51f3ba93658 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Sat, 10 Jul 2021 00:16:23 +0200 Subject: [PATCH 47/96] #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 48/96] #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 49/96] #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 50/96] #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 51/96] #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 52/96] #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 53/96] #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 54/96] 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 55/96] #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 56/96] 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 7f0f419d675885d654c06a6e4b2df9e4a320b0d2 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 13 Jul 2021 16:54:30 +0100 Subject: [PATCH 57/96] Fix layout bug in the share extension. --- .../Modules/Share/Listing/RoomsListViewController.m | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m index 777249a6e..94acaa453 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m +++ b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m @@ -129,6 +129,13 @@ } } +- (void)setKeyboardHeight:(CGFloat)keyboardHeight +{ + // Bypass inherited keyboard handling to fix layout when searching. + // There are no sticky headers to worry about updating. + return; +} + #pragma mark - Private - (void)showShareAlertForRoomPath:(NSIndexPath *)indexPath @@ -289,6 +296,12 @@ // Refresh display [self refreshRecentsTable]; } + + // Dismiss the keyboard when scrolling to match the behaviour of the main app. + if (self.recentsSearchBar.isFirstResponder) + { + [self.recentsSearchBar resignFirstResponder]; + } } } } From 3d900e3068c65b06f9e5c1c1409dbda183208948 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 13 Jul 2021 17:08:48 +0100 Subject: [PATCH 58/96] Update CHANGES.rst. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2e72d9c8e..75dccc3c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,7 @@ Changes to be released in next version * 🐛 Bugfix - * + * Share Extension: Fix layout when searching (#4258). ⚠️ API Changes * From 651f424d67dafd5daa9e0b7a3d581c6e79b6e306 Mon Sep 17 00:00:00 2001 From: TheBlueKingLP <*@tbk.gg> Date: Mon, 12 Jul 2021 15:38:45 +0000 Subject: [PATCH 59/96] Translated using Weblate (Japanese) Currently translated at 91.4% (32 of 35 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/ja/ --- Riot/Assets/ja.lproj/Localizable.strings | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Riot/Assets/ja.lproj/Localizable.strings b/Riot/Assets/ja.lproj/Localizable.strings index 51a5c3d85..a2cbb08e6 100644 --- a/Riot/Assets/ja.lproj/Localizable.strings +++ b/Riot/Assets/ja.lproj/Localizable.strings @@ -50,3 +50,27 @@ "SINGLE_UNREAD_IN_ROOM" = "%@にメッセージを受け取りました"; /* A single unread message */ "SINGLE_UNREAD" = "あなたはメッセージを受け取りました"; + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@は検証したい"; + +/* New message indicator on a room */ +"MESSAGE_IN_X" = "%@ 内のメッセージ"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ さんからのスタンプ"; +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@(%@ から)"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (グループ通話)"; +"MESSAGE_PROTECTED" = "新しいメッセージ"; + +/* New message indicator from a DM */ +"MESSAGE_FROM_X" = "%@ からのメッセージ"; + +/** Notification messages **/ + +/* New message indicator on unknown room */ +"MESSAGE" = "メッセージ"; From b70c5b02c69a16005efb5ec3285cb5b3190dfd88 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 14 Jul 2021 10:26:21 +0300 Subject: [PATCH 60/96] #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 fa1bbecf26d0edf6311ab8298ad2f85a10dabce0 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 14 Jul 2021 10:15:50 +0100 Subject: [PATCH 61/96] Always update the path of the attachmentView's mask to handle reuse. --- CHANGES.rst | 1 + Riot/Modules/Room/DataSources/RoomDataSource.m | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bc28f2c30..4f9ab91a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,7 @@ Changes to be released in next version * Use different title for scan button for self verification (#4525). * it's easy for the back button to trigger a leftpanel reveal (#4438). * Show / hide reset button in secrets recovery screen (#4546). + * Timeline: Fix incorrect crop of media thumbnails. ⚠️ API Changes * diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 230d313ce..6ab7ec1bf 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -1039,10 +1039,21 @@ const CGFloat kTypingCellHeight = 24; - (void)applyMaskToAttachmentViewOfBubbleCell:(MXKRoomBubbleTableViewCell *)cell { - if (cell.attachmentView && !cell.attachmentView.layer.mask) + if (cell.attachmentView) { UIBezierPath *myClippingPath = [UIBezierPath bezierPathWithRoundedRect:cell.attachmentView.bounds cornerRadius:6]; - CAShapeLayer *mask = [CAShapeLayer layer]; + CAShapeLayer *mask; + + // check for an existing mask in case the cell is being reused, otherwise create a new one + if ([cell.attachmentView.layer.mask isKindOfClass:[CAShapeLayer class]]) + { + mask = (CAShapeLayer*)cell.attachmentView.layer.mask; + } + else + { + mask = [CAShapeLayer layer]; + } + mask.path = myClippingPath.CGPath; cell.attachmentView.layer.mask = mask; } From 445db8ba78128f91436c9efc5ee44b62708559a3 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 14 Jul 2021 10:37:54 +0100 Subject: [PATCH 62/96] Use the layer's corner radius rather than a custom mask. This will ensure the mask bounds always match the view's size. --- Riot/Modules/Room/DataSources/RoomDataSource.m | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 6ab7ec1bf..434aee6ac 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -1041,21 +1041,8 @@ const CGFloat kTypingCellHeight = 24; { if (cell.attachmentView) { - UIBezierPath *myClippingPath = [UIBezierPath bezierPathWithRoundedRect:cell.attachmentView.bounds cornerRadius:6]; - CAShapeLayer *mask; - - // check for an existing mask in case the cell is being reused, otherwise create a new one - if ([cell.attachmentView.layer.mask isKindOfClass:[CAShapeLayer class]]) - { - mask = (CAShapeLayer*)cell.attachmentView.layer.mask; - } - else - { - mask = [CAShapeLayer layer]; - } - - mask.path = myClippingPath.CGPath; - cell.attachmentView.layer.mask = mask; + cell.attachmentView.layer.cornerRadius = 6; + cell.attachmentView.layer.masksToBounds = YES; } } From 87d62650673cf6a103b663f4dea5d478e7cffed9 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 14 Jul 2021 14:40:54 +0300 Subject: [PATCH 63/96] #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 4f9b05492e02893f85df5b698f0741dd3a8c92a2 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 15 Jul 2021 20:11:39 +0200 Subject: [PATCH 64/96] Changes: Restore 1.4.5 changes. --- CHANGES.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3bb978f30..bdb4f7f03 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,30 @@ Changes to be released in next version Others * Silenced some documentation, deprecations and SwiftLint warnings. + +Changes in 1.4.5 (2021-07-07) +================================================= + +✨ Features + * + +🙌 Improvements + * + +🐛 Bugfix + * Notifications: Fix an issue where the app is unresponsive after getting some notifications (#4534). + +⚠️ API Changes + * + +🗣 Translations + * + +🧱 Build + * + +Others + * Changes in 1.4.4 (2021-06-30) ================================================= From b0c48ecee64bd71e01a19617bf6f2a9bae5d7f96 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 09:06:36 +0300 Subject: [PATCH 65/96] #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 66/96] #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 67/96] #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 68/96] 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 69/96] #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 70/96] 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 71/96] #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 6af882ac4c3bbd4a7c98bbf1e91af3658880245e Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 16 Jul 2021 15:13:45 +0200 Subject: [PATCH 72/96] version++ --- CHANGES.rst | 5 ++++- Config/AppIdentifiers.xcconfig | 4 ++-- Podfile | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bdb4f7f03..8b6537681 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -Changes to be released in next version +Changes in 1.4.6 (2021-07-16) ================================================= ✨ Features @@ -32,6 +32,9 @@ Changes to be released in next version Others * Silenced some documentation, deprecations and SwiftLint warnings. +Improvements: + * Upgrade MatrixKit version ([v0.15.4](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.15.4)). + Changes in 1.4.5 (2021-07-07) ================================================= diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index 391931749..758906cac 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.5 -CURRENT_PROJECT_VERSION = 1.4.5 +MARKETING_VERSION = 1.4.6 +CURRENT_PROJECT_VERSION = 1.4.6 // Team diff --git a/Podfile b/Podfile index d008c31f2..2c4471394 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.15.3' +$matrixKitVersion = '= 0.15.4' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} From c0067a81b32390f301abc15a54a660e5b0d81bdd Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 16 Jul 2021 16:57:29 +0300 Subject: [PATCH 73/96] #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 158eab81ddfcb270ff985efa2e58dcb340c93fb4 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 16 Jul 2021 16:59:24 +0200 Subject: [PATCH 74/96] finish version++ --- Podfile.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 8c6193dfa..8b171f979 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -56,29 +56,29 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.15.3): + - MatrixKit (0.15.4): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.15.3) - - MatrixSDK (= 0.19.3) - - MatrixKit/Core (0.15.3): + - MatrixKit/Core (= 0.15.4) + - MatrixSDK (= 0.19.4) + - MatrixKit/Core (0.15.4): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.19.3) - - MatrixSDK (0.19.3): - - MatrixSDK/Core (= 0.19.3) - - MatrixSDK/Core (0.19.3): + - MatrixSDK (= 0.19.4) + - MatrixSDK (0.19.4): + - MatrixSDK/Core (= 0.19.4) + - MatrixSDK/Core (0.19.4): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.4) - Realm (= 10.7.6) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.19.3): + - MatrixSDK/JingleCallStack (0.19.4): - JitsiMeetSDK (= 3.5.0) - MatrixSDK/Core - OLMKit (3.2.4): @@ -120,7 +120,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.15.3) + - MatrixKit (= 0.15.4) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -196,8 +196,8 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: 6cbe65db11a5450ec8cc02d51660f43b5e95a141 - MatrixSDK: c15663c67bfd2991d897d973c1551ba4de900e25 + MatrixKit: 477690d6dd38b1526042f8e83ff34e3db87e3a00 + MatrixSDK: 0c9d1845a45d71abcfcabe82b706cadbda1297ae OLMKit: 2d73cd67d149b5c3e3a8eb8ecae93d0b429d8a02 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: ed860452717c8db8f4bf832b6807f7f2ce708839 @@ -211,6 +211,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: c39d88adc5ec2df412af32b64ceb99a9a1ee92a8 +PODFILE CHECKSUM: 2b61b852a5b62fbb75643a30281e362fd4566f12 COCOAPODS: 1.10.1 From a1a47ff5a385e6b1a287bc283f0e6112edbb489f Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 16 Jul 2021 17:26:45 +0200 Subject: [PATCH 75/96] finish version++ From 6e91252f9e99230b140e6396c37a9de792304c2d Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 16 Jul 2021 17:26:50 +0200 Subject: [PATCH 76/96] 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 8b6537681..be5868916 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.6 (2021-07-16) ================================================= diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index 758906cac..a5e2e60c9 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.6 -CURRENT_PROJECT_VERSION = 1.4.6 +MARKETING_VERSION = 1.4.7 +CURRENT_PROJECT_VERSION = 1.4.7 // Team From aa3266da8223e6ed8cfba469dbba50a463dcd50a Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 16 Jul 2021 15:50:46 +0000 Subject: [PATCH 77/96] Translated using Weblate (German) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 52afbc691..7789548c4 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1389,3 +1389,16 @@ "room_recents_unknown_room_error_message" = "Raum kann nicht gefunden werden. Überprüfe bitte, dass er existiert"; "room_creation_dm_error" = "Fehler beim Erstellen der Direktnachricht. Bitte überprüfe die eingeladenen Leute und versuche es erneut."; "settings_ui_theme_picker_message_invert_colours" = "\"Auto\" verwendet die Farbinvertierungseinstellung deines Geräts"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Mit diesem Gerät scannen"; +"room_notifs_settings_encrypted_room_notice" = "Am Handy sind Benachrichtigungen bei Erwähnungen und Schlüsselwörtern in verschlüsselten Räumen nicht verfügbar."; +"room_notifs_settings_account_settings" = "Kontoeinstellungen"; +"room_notifs_settings_manage_notifications" = "Benachrichtigungen kannst du in %@ verwalten"; +"room_notifs_settings_cancel_action" = "Abbrechen"; +"room_notifs_settings_done_action" = "Fertig"; +"room_notifs_settings_none" = "Nichts"; +"room_notifs_settings_mentions_and_keywords" = "Nur Erwähnungen und Schlüsselwörtern"; +"room_notifs_settings_all_messages" = "Allen Nachrichten"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Benachrichtige mich bei"; +"room_details_notifs" = "Benachrichtigungen"; From ae2364e9a6f8c1ad7c284b9025bd3fead246b5ad Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Jul 2021 10:11:10 +0000 Subject: [PATCH 78/96] Translated using Weblate (Albanian) Currently translated at 99.6% (1240 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 0794ad097..aac94c32c 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -1401,3 +1401,16 @@ "settings_ui_theme_picker_message_invert_colours" = "“Auto” përdor rregullimet “Ktheji Së Prapthi Ngjyrat”"; "room_recents_unknown_room_error_message" = "S’gjendet dot kjo dhomë. Sigurohuni se ekziston"; "room_creation_dm_error" = "S’mundëm të krijojmë dot MD-në tuaj. Ju lutemi, kontrolloni përdoruesit të cilëve doni t’u dërgohet dhe riprovoni."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Skanoje me këtë pajisje"; +"room_notifs_settings_encrypted_room_notice" = "Ju lutemi, kini parasysh se njoftimet për përmendje & fjalëkyçe s’mund të kihen në celular për dhoma të fshehtëzuara."; +"room_notifs_settings_account_settings" = "Rregullime llogarie"; +"room_notifs_settings_manage_notifications" = "Njoftimet mund t’i administroni që nga %@"; +"room_notifs_settings_cancel_action" = "Anuloje"; +"room_notifs_settings_done_action" = "U bë"; +"room_notifs_settings_none" = "Asnjë"; +"room_notifs_settings_mentions_and_keywords" = "Vetëm Përmendje dhe Fjalëkyçe"; +"room_notifs_settings_all_messages" = "Krejt Mesazhet"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Njoftomëni për"; +"room_details_notifs" = "Njoftime"; From d42d7b8f3c75e13a51e59384f95334a789486704 Mon Sep 17 00:00:00 2001 From: random Date: Sat, 17 Jul 2021 15:00:52 +0000 Subject: [PATCH 79/96] Translated using Weblate (Italian) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index b6a2f0829..3a0fd2639 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1383,3 +1383,16 @@ "settings_ui_theme_picker_message_invert_colours" = "\"Automatico\" usa l'impostazione \"Inverti Colori\" del tuo dispositivo"; "room_recents_unknown_room_error_message" = "Impossibile trovare questa stanza. Assicurati che esista"; "room_creation_dm_error" = "Impossibile creare il messaggio diretto. Ricontrolla gli utenti che vuoi invitare e riprova."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Scansiona con questo dispositivo"; +"room_notifs_settings_encrypted_room_notice" = "Nota che le notifiche per menzioni e parole chiave non sono disponibili nelle stanze cifrate su mobile."; +"room_notifs_settings_account_settings" = "impostazioni dell'account"; +"room_notifs_settings_manage_notifications" = "Puoi gestire le notifiche nelle %@"; +"room_notifs_settings_cancel_action" = "Annulla"; +"room_notifs_settings_done_action" = "Fatto"; +"room_notifs_settings_none" = "Niente"; +"room_notifs_settings_mentions_and_keywords" = "Solo menzioni e parole chiave"; +"room_notifs_settings_all_messages" = "Tutti i messaggi"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Inviami notifiche per"; +"room_details_notifs" = "Notifiche"; From 15c8d3d327c0f2ffffc8a243acebe3a15f0d66cb Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Thu, 15 Jul 2021 17:47:04 +0000 Subject: [PATCH 80/96] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 9a9f7396b..305fac89f 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1380,3 +1380,16 @@ "settings_ui_theme_picker_message_invert_colours" = "\"Auto\" usa as configurações \"Inverter Cores\" de seu dispositivo"; "room_recents_unknown_room_error_message" = "Não dá para encontrar esta sala. Assegure que ela existe"; "room_creation_dm_error" = "Nós não conseguimos criar sua DM. Por favor cheque as/os usuárias(os) que você quer convidar e tente de novo."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Scannar com este dispositivo"; +"room_notifs_settings_encrypted_room_notice" = "Por favor note que notificações de menções & palavrachave não estão disponíveis em salas encriptadas no celular."; +"room_notifs_settings_account_settings" = "Configurações de conta"; +"room_notifs_settings_manage_notifications" = "Você pode gerenciar notificações em %@"; +"room_notifs_settings_cancel_action" = "Cancelar"; +"room_notifs_settings_done_action" = "Feito"; +"room_notifs_settings_none" = "Nenhuma"; +"room_notifs_settings_mentions_and_keywords" = "Menções e Palavrachaves somente"; +"room_notifs_settings_all_messages" = "Todas as Mensagens"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Notifique-me para"; +"room_details_notifs" = "Notificações"; From 1b042db4d326c9c184ef1e4c83abbb62079dd7dc Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 19 Jul 2021 15:40:17 +0300 Subject: [PATCH 81/96] #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 82/96] #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 83/96] 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 1e02f528517117a5adf2c059f0c9fe6c1ae3e992 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 21 Jul 2021 14:07:45 +0000 Subject: [PATCH 84/96] Translated using Weblate (Japanese) Currently translated at 69.2% (861 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index a3d3a68e7..5ce1c41d7 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -307,7 +307,7 @@ "settings_labs_matrix_apps" = "Matrixアプリ"; "settings_labs_create_conference_with_jitsi" = "jitsiの会議通話を作成する"; "settings_version" = "Version %@"; -"settings_olm_version" = "Olm Version %@"; +"settings_olm_version" = "Olm バージョン %@"; "settings_copyright" = "著作権"; "settings_term_conditions" = "利用規約"; "settings_privacy_policy" = "個人情報保護方針"; @@ -323,7 +323,7 @@ "settings_password_updated" = "あなたのパスワードは更新されました"; "settings_crypto_device_name" = "セッション名: "; "settings_crypto_device_id" = "\nセッションID: "; -"settings_crypto_device_key" = "\n端末鍵: "; +"settings_crypto_device_key" = "\nセッションキー:\n"; "settings_crypto_export" = "暗号鍵を外部へ保存"; "settings_crypto_blacklist_unverified_devices" = "検証されたセッションのみで暗号化"; // Room Details @@ -534,7 +534,7 @@ "e2e_room_key_request_title" = "暗号化キー要求"; "e2e_room_key_request_message_new_device" = "暗号化キーを要求している新しい端末 '%@'を追加しました。"; "e2e_room_key_request_message" = "検証されていない端末 '%@'が暗号化キーを要求しています。"; -"e2e_room_key_request_start_verification" = "検証開始..."; +"e2e_room_key_request_start_verification" = "検証を始めます…"; "e2e_room_key_request_share_without_verifying" = "検証せずに共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; // GDPR @@ -556,7 +556,7 @@ "rerequest_keys_alert_title" = "リクエスト送信"; "rerequest_keys_alert_message" = "この端末にキーを送信できるように、メッセージを復号化できる別の端末でElementを起動してください。"; "room_event_action_ban_prompt_reason" = "このユーザーをブロックする理由"; -"room_resource_limit_exceeded_message_contact_1" = " Please "; +"room_resource_limit_exceeded_message_contact_1" = " お願い "; "settings_ui_theme_black" = "Black"; "settings_flair" = "特色を表示する"; // String for App Store @@ -753,7 +753,7 @@ "room_participants_leave_prompt_msg_for_dm" = "退出してよろしいですか?"; "room_participants_leave_prompt_title_for_dm" = "退出する"; "contacts_address_book_no_identity_server" = "IDサーバーが設定されていません"; -"rooms_empty_view_information" = "ルームはプライベートでもパブリックでも、あらゆるグループチャットに最適です。+をタップすると既にある部屋を探したり、新しい部屋を作ることができます。"; +"rooms_empty_view_information" = "ルームはプライベートでもパブリックでも、あらゆるグループチャットに最適です。+をタップすると、既にある部屋を見つけたり、新しい部屋を作ることができます。"; "rooms_empty_view_title" = "ルーム"; "people_empty_view_information" = "誰とでも安全にチャットできます。+をタップすると会話相手を追加できます。"; "people_empty_view_title" = "人々"; From 057d7af5a458a957c7e0ba42a216c3aab08da197 Mon Sep 17 00:00:00 2001 From: metatek Date: Wed, 21 Jul 2021 14:06:40 +0000 Subject: [PATCH 85/96] Translated using Weblate (Japanese) Currently translated at 69.2% (861 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 5ce1c41d7..740e0a244 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2,7 +2,7 @@ "title_home" = "ホーム"; "title_favourites" = "お気に入り"; "title_people" = "対話"; -"title_rooms" = "部屋"; +"title_rooms" = "ルーム"; "warning" = "警告"; // Actions "view" = "表示"; @@ -92,37 +92,37 @@ "room_creation_account" = "アカウント"; "room_creation_appearance" = "外観"; "room_creation_appearance_name" = "名前"; -"room_creation_appearance_picture" = "部屋のアイコン画像 (任意で)"; +"room_creation_appearance_picture" = "チャット画像 (任意)"; "room_creation_privacy" = "個人情報保護"; "room_creation_private_room" = "この会話は非公開です"; "room_creation_public_room" = "この会話は公開されます"; -"room_creation_make_public" = "公開部屋作成"; -"room_creation_make_public_prompt_title" = "この部屋を公開しますか?"; -"room_creation_make_public_prompt_msg" = "この部屋を一般公開してもよろしいですか?誰でもあなたの発言を読み、部屋に参加できます。"; -"room_creation_keep_private" = "非公開部屋"; -"room_creation_make_private" = "非公開部屋を作成"; -"room_creation_wait_for_creation" = "部屋はすでに作成されています。お待ちください。"; +"room_creation_make_public" = "パブリックにする"; +"room_creation_make_public_prompt_title" = "このチャットをパブリックしますか?"; +"room_creation_make_public_prompt_msg" = "このチャットをパブリックしてもよろしいですか? 誰でもあなたのメッセージを読んでチャットに参加できます。"; +"room_creation_keep_private" = "プライベートに保つ"; +"room_creation_make_private" = "プライベートにする"; +"room_creation_wait_for_creation" = "ルームはすでに作成されています。 お待ちください。"; "room_creation_invite_another_user" = "ユーザID, 表示名, 電子メールで検索と招待"; // Room recents -"room_recents_directory_section" = "部屋一覧"; +"room_recents_directory_section" = "ルーム一覧"; "room_recents_directory_section_network" = "通信回線"; "room_recents_favourites_section" = "お気に入り"; "room_recents_people_section" = "対話"; -"room_recents_conversations_section" = "部屋"; -"room_recents_no_conversation" = "部屋がありません"; +"room_recents_conversations_section" = "ルーム"; +"room_recents_no_conversation" = "ルームがありません"; "room_recents_low_priority_section" = "低優先度"; "room_recents_invites_section" = "招待中"; "room_recents_start_chat_with" = "対話を開始"; -"room_recents_create_empty_room" = "部屋を作成"; -"room_recents_join_room" = "部屋へ参加"; -"room_recents_join_room_title" = "部屋へ参加"; -"room_recents_join_room_prompt" = "部屋の固有IDまたは住所表記を入力"; +"room_recents_create_empty_room" = "ルームを作成"; +"room_recents_join_room" = "ルームへ参加"; +"room_recents_join_room_title" = "ルームへ参加"; +"room_recents_join_room_prompt" = "ルームIDまたはルームのエイリアスを入力します"; // People tab "people_invites_section" = "招待中"; "people_conversation_section" = "会話"; "people_no_conversation" = "会話なし"; // Rooms tab -"room_directory_no_public_room" = "公開された部屋がありません"; +"room_directory_no_public_room" = "利用可能なパブリックのルームはありません"; // Search "search_rooms" = "部屋"; "search_messages" = "発言"; @@ -354,7 +354,7 @@ "room_details_addresses_section" = "住所表記"; "room_details_no_local_addresses" = "この部屋はサーバ内住所表記がありません"; "room_details_new_address" = "新しい住所表記を追加"; -"room_details_new_address_placeholder" = "新しい住所表記を追加 (例 #foo%@)"; +"room_details_new_address_placeholder" = "新しいアドレスを追加(例 #foo%@)"; "room_details_addresses_invalid_address_prompt_title" = "住所表記が正しくありません"; "room_details_addresses_invalid_address_prompt_msg" = "%@ は正しくない形式の住所表記です"; "room_details_addresses_disable_main_address_prompt_title" = "代表住所表記の警告"; From cc62b18b7d28781c79dad3176f74c195141a6bc7 Mon Sep 17 00:00:00 2001 From: jelv Date: Tue, 20 Jul 2021 11:13:33 +0000 Subject: [PATCH 86/96] Translated using Weblate (Dutch) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ --- Riot/Assets/nl.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 1b5064d3e..661b7a890 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1512,3 +1512,16 @@ "settings_ui_theme_picker_message_invert_colours" = "‘Automatisch’ gebruikt de instelling ‘Kleurweergave omkeren’ van uw apparaat"; "room_recents_unknown_room_error_message" = "Dit gesprek is niet gevonden. Controleer of het bestaat"; "room_creation_dm_error" = "Uw direct gesprek kon niet aangemaakt worden. Controleer de gebruikers die u wilt uitnodigen en probeer het opnieuw."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Scan met dit apparaat"; +"room_notifs_settings_encrypted_room_notice" = "Let op dat vermeldingen & trefwoorden-meldingen niet beschikbaar zijn in versleutelde gesprekken op mobiel."; +"room_notifs_settings_account_settings" = "Accountinstellingen"; +"room_notifs_settings_manage_notifications" = "U kunt uw meldingen beheren in %@"; +"room_notifs_settings_cancel_action" = "Annuleer"; +"room_notifs_settings_done_action" = "Klaar"; +"room_notifs_settings_none" = "Geen"; +"room_notifs_settings_mentions_and_keywords" = "Alleen vermeldingen en trefwoorden"; +"room_notifs_settings_all_messages" = "Alle berichten"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Stuur een melding voor"; +"room_details_notifs" = "Meldingen"; From 45d427212057a4845d98538178602cf2451dcccf Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 21 Jul 2021 06:21:59 +0000 Subject: [PATCH 87/96] Translated using Weblate (German) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 7789548c4..6cb67c557 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -506,7 +506,7 @@ "group_rooms_filter_rooms" = "Filtere Community-Räume"; "e2e_room_key_request_message_new_device" = "Du hast die neue Sitzung '%@' hinzugefügt, welche Verschlüsselungs-Schlüssel anfordert."; "room_do_not_have_permission_to_post" = "Du hast keine Berechtigung Nachrichten in diesem Raum zu senden"; -"room_event_action_kick_prompt_reason" = "Grund für den Rauswurf des Benutzers"; +"room_event_action_kick_prompt_reason" = "Grund für das Entfernen des Benutzers"; "room_event_action_ban_prompt_reason" = "Grund für die Verbannung des Benutzers"; "room_action_send_photo_or_video" = "Foto oder Video senden"; "room_action_send_sticker" = "Aufkleber senden"; From ccf14548b9dfd5e1f17bbb4d91781f7df599ed8a Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Mon, 19 Jul 2021 09:29:30 +0000 Subject: [PATCH 88/96] Translated using Weblate (French) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ --- Riot/Assets/fr.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index f78010dfe..8e250f532 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -1426,3 +1426,16 @@ "settings_ui_theme_picker_message_invert_colours" = "« Auto » utilise le paramètre « Inverser les couleurs » de l’appreil"; "room_recents_unknown_room_error_message" = "Aucun résultat dans ce salon. Assurez vous de son existence"; "room_creation_dm_error" = "Nous n’avons pas pu créer votre message direct. Merci de vérifier les utilisateurs que vous voulez inviter et réessayer."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Scanner avec cet appareil"; +"room_notifs_settings_encrypted_room_notice" = "Veuillez noter que les mentions et mots-clés ne sont pas disponibles dans les salons chiffrés sur mobile."; +"room_notifs_settings_account_settings" = "Paramètres du compte"; +"room_notifs_settings_manage_notifications" = "Vous pouvez gérer les notifications dans %@"; +"room_notifs_settings_cancel_action" = "Annuler"; +"room_notifs_settings_done_action" = "Terminé"; +"room_notifs_settings_none" = "Aucun"; +"room_notifs_settings_mentions_and_keywords" = "Seulement les mentions et les mots-clés"; +"room_notifs_settings_all_messages" = "Tous les messages"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Me notifier pour"; +"room_details_notifs" = "Notifications"; From 38973fbd18aeb09a69fa86f310e0309a4b155f6c Mon Sep 17 00:00:00 2001 From: metatek Date: Wed, 21 Jul 2021 14:17:46 +0000 Subject: [PATCH 89/96] Translated using Weblate (Japanese) Currently translated at 69.2% (861 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 38 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 740e0a244..a77584240 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -124,7 +124,7 @@ // Rooms tab "room_directory_no_public_room" = "利用可能なパブリックのルームはありません"; // Search -"search_rooms" = "部屋"; +"search_rooms" = "ルーム"; "search_messages" = "発言"; "search_people" = "対話"; "search_files" = "添付ファイル"; @@ -133,12 +133,12 @@ "search_no_result" = "結果なし"; "search_in_progress" = "検索中…"; // Directory -"directory_cell_title" = "部屋一覧を見る"; -"directory_cell_description" = "%tu 部屋"; -"directory_search_results_title" = "部屋一覧検索結果"; +"directory_cell_title" = "ルーム一覧を見る"; +"directory_cell_description" = "%tu ルーム"; +"directory_search_results_title" = "ルーム一覧検索結果"; "directory_search_results" = "%tu 件の検索結果 for %@"; "directory_search_results_more_than" = ">%tu 件の検索結果 for %@"; -"directory_searching_title" = "部屋一覧を検索中…"; +"directory_searching_title" = "ルーム一覧を検索中…"; "directory_search_fail" = "一覧を取得できませんでした"; // Contacts "contacts_address_book_section" = "端末の電話帳"; @@ -153,13 +153,13 @@ "room_participants_add_participant" = "参加者を追加"; "room_participants_one_participant" = "参加者 1名"; "room_participants_multi_participants" = "参加者 %d名"; -"room_participants_leave_prompt_title" = "部屋を退室"; -"room_participants_leave_prompt_msg" = "部屋を退室して本当によろしいですか?"; +"room_participants_leave_prompt_title" = "ルームを出る"; +"room_participants_leave_prompt_msg" = "ルームを退室して本当によろしいですか?"; "room_participants_remove_prompt_title" = "確認"; -"room_participants_remove_prompt_msg" = "本当に %@ さんを部屋から退去させますか?"; +"room_participants_remove_prompt_msg" = "本当に %@ さんをチャットから退去させますか?"; "room_participants_remove_third_party_invite_msg" = "サードパーティの招待を削除することは、APIが存在するまでサポートされていません"; "room_participants_invite_prompt_title" = "確認"; -"room_participants_invite_prompt_msg" = "本当に %@ さんを部屋へ招待しますか?"; +"room_participants_invite_prompt_msg" = "本当に %@ さんをチャットへ招待しますか?"; "room_participants_filter_room_members" = "参加者を検索"; "room_participants_invite_another_user" = "ユーザID, 表示名, 電子メールで検索と招待"; "room_participants_invite_malformed_id_title" = "招待エラー"; @@ -176,9 +176,9 @@ "room_participants_action_section_devices" = "セッション"; "room_participants_action_section_other" = "オプション"; "room_participants_action_invite" = "招待"; -"room_participants_action_leave" = "部屋を退室"; -"room_participants_action_remove" = "部屋から退室させる"; -"room_participants_action_ban" = "この部屋からブロックする"; +"room_participants_action_leave" = "このルームを出る"; +"room_participants_action_remove" = "このルームから退室させる"; +"room_participants_action_ban" = "このルームからブロックする"; "room_participants_action_unban" = "ブロック解除"; "room_participants_action_ignore" = "この参加者の発言を全て非表示にする"; "room_participants_action_unignore" = "この参加者の発言を全て表示する"; @@ -205,7 +205,7 @@ "room_ongoing_conference_call" = "会議通話実施中。 %@ または %@で参加してください。"; "room_ongoing_conference_call_with_close" = "会議通話実施中。%@または%@で参加してください。 %@。"; "room_ongoing_conference_call_close" = "閉じる"; -"room_conference_call_no_power" = "この部屋で会議通話を管理する権限が必要です"; +"room_conference_call_no_power" = "このルームで会議通話を管理する権限が必要です"; "room_prompt_resend" = "全て再送信"; "room_prompt_cancel" = "全て中止"; "room_resend_unsent_messages" = "未送信の文を再送信"; @@ -473,14 +473,14 @@ "group_invite_section" = "招待"; "group_section" = "コミュニティ"; "room_message_reply_to_placeholder" = "返信を送る (暗号化されていない)…"; -"room_do_not_have_permission_to_post" = "この部屋に投稿する権限がありません"; +"room_do_not_have_permission_to_post" = "このルームに投稿する権限がありません"; "encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送る…"; "room_message_reply_to_short_placeholder" = "返信を送る…"; "room_event_action_view_decrypted_source" = "復号化されたソースを見る"; "room_event_action_kick_prompt_reason" = "このユーザーを追放する理由"; "room_action_send_photo_or_video" = "写真か動画を送る"; "room_action_send_sticker" = "スタンプ送信"; -"room_replacement_information" = "この部屋は交換されており、もうアクティブではありません。"; +"room_replacement_information" = "このルームは交換されており、もうアクティブではありません。"; "room_replacement_link" = "会話はここで続けられます。"; "room_predecessor_information" = "この部屋は別の会話の続きです。"; "room_predecessor_link" = "より古いメッセージを見るにはここをタップしてください。"; @@ -599,8 +599,8 @@ // Mark: - Room creation introduction cell "room_intro_cell_add_participants_action" = "参加者を追加"; -"room_participants_security_information_room_encrypted" = "この部屋で送受信されるメッセージはエンドツーエンド暗号化されます。\n\nメッセージは安全に保護されており、この部屋の参加者のみがメッセージの閲覧に必要な鍵を所持します。"; -"room_participants_security_information_room_not_encrypted" = "この部屋で送受信されるメッセージはエンドツーエンド暗号化されません。"; +"room_participants_security_information_room_encrypted" = "このルームで送受信されるメッセージはエンドツーエンド暗号化されます。\n\nメッセージは安全に保護されており、このルームの参加者のみがメッセージの閲覧に必要な鍵を所持します。"; +"room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されていません。"; "room_intro_cell_information_dm_sentence1_part3" = ". "; "callbar_active_and_single_paused" = "ひとつのアクティブな通話 (%@) · ひとつの一時停止された通話"; @@ -729,7 +729,7 @@ "room_multiple_typing_notification" = "%@とその他のユーザーが入力中です"; "external_link_confirmation_message" = "リンク%@は別のサイトに移動します: %@\n\n本当に続けますか?"; "room_event_action_delete_confirmation_title" = "未送信メッセージを削除"; -"room_unsent_messages_cancel_message" = "この部屋にある未送信のメッセージをすべて削除してもよろしいですか?"; +"room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージをすべて削除してもよろしいですか?"; "room_unsent_messages_cancel_title" = "未送信メッセージを削除"; "room_message_replying_to" = "%@に返信中"; "room_message_editing" = "編集中"; @@ -753,7 +753,7 @@ "room_participants_leave_prompt_msg_for_dm" = "退出してよろしいですか?"; "room_participants_leave_prompt_title_for_dm" = "退出する"; "contacts_address_book_no_identity_server" = "IDサーバーが設定されていません"; -"rooms_empty_view_information" = "ルームはプライベートでもパブリックでも、あらゆるグループチャットに最適です。+をタップすると、既にある部屋を見つけたり、新しい部屋を作ることができます。"; +"rooms_empty_view_information" = "ルームはプライベートでもパブリックでも、あらゆるグループチャットに最適です。+をタップすると、既にあるルームを見つけたり、新しいルームを作ることができます。"; "rooms_empty_view_title" = "ルーム"; "people_empty_view_information" = "誰とでも安全にチャットできます。+をタップすると会話相手を追加できます。"; "people_empty_view_title" = "人々"; From 3cf1c2582d16e6ebc4f95ac05fdcd6062dc128d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 18 Jul 2021 20:15:21 +0000 Subject: [PATCH 90/96] Translated using Weblate (Estonian) Currently translated at 100.0% (1244 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 32ede1f7f..fb751e153 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1349,3 +1349,16 @@ "settings_ui_theme_picker_message_invert_colours" = "Automaatne valik kasutab sinu seadme pööratud värvide seadistust"; "room_recents_unknown_room_error_message" = "Ei leia sellist jututuba. Palun kontrolli, et ta ikka olemas on"; "room_creation_dm_error" = "Otsevestluse loomine ei õnnestunud. Palun kontrolli, et kasutajanimed oleks õiged ja proovi uuesti."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Skaneeri selle seadmega"; +"room_notifs_settings_encrypted_room_notice" = "Teavitused mainimiste ja võtmesõnade esinemise puhul pole mobiilirakenduses krüptitud jututoas saadaval."; +"room_notifs_settings_account_settings" = "Kasutajakonto seadistused"; +"room_notifs_settings_manage_notifications" = "Sa võid hallata teavitusi %@ jututoas"; +"room_notifs_settings_cancel_action" = "Katkesta"; +"room_notifs_settings_done_action" = "Valmis"; +"room_notifs_settings_none" = "mitte ühelgi juhul"; +"room_notifs_settings_mentions_and_keywords" = "mainimiste ja võtmesõnade leidumise puhul"; +"room_notifs_settings_all_messages" = "kõikide sõnumite puhul"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Teavita mind"; +"room_details_notifs" = "Teavitused"; From 41b0d480ecae470f18145ba3633eb54ded2c9fc7 Mon Sep 17 00:00:00 2001 From: zer0-x <1rn0kmrwo@relay.firefox.com> Date: Tue, 20 Jul 2021 08:56:22 +0000 Subject: [PATCH 91/96] Translated using Weblate (Arabic) Currently translated at 24.3% (303 of 1244 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ar/ --- Riot/Assets/ar.lproj/Vector.strings | 85 +++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/Riot/Assets/ar.lproj/Vector.strings b/Riot/Assets/ar.lproj/Vector.strings index 8687e7f26..7aa1dddd8 100644 --- a/Riot/Assets/ar.lproj/Vector.strings +++ b/Riot/Assets/ar.lproj/Vector.strings @@ -261,3 +261,88 @@ "room_participants_filter_room_members_for_dm" = "تَصفِيَةُ الأعضَاء"; "room_participants_filter_room_members" = "تَصفِيَةُ أعضَاءِ الغُرفَة"; "room_participants_invite_prompt_msg" = "هَل أنتَ مُتَأكِّدٌ أنَّكَ تٌريدُ دَعوةَ %@ إلَى هَذِهِ المُحادَثَة؟"; +"room_event_action_reaction_show_less" = "إظهَارُ أقَل"; +"room_event_action_reaction_show_all" = "إظهَارُ الكُل"; +"room_event_action_edit" = "تَحرِير"; +"room_event_action_reply" = "الرَّدّ"; +"room_event_action_view_encryption" = "مَعلُومَاتُ التَّعمِيَة"; +"room_event_action_cancel_download" = "إلغَاءُ التَّنزيل"; +"room_event_action_cancel_send" = "إلغَاءُ الإرسَال"; +"room_event_action_delete_confirmation_message" = "هَل أنتَ مُتَأكِّدٌ أنَّكَ تٌريدُ حَذفَ هَذِهِ الرِّسَالَةَ غَيرِ المُرسَلَة؟"; +"room_event_action_delete_confirmation_title" = "حَذفُ الرَّسَائِلِ غَيرِ المُرسَلَة"; +"room_event_action_delete" = "حَذف"; +"room_event_action_resend" = "إعادَةُ الإرسَال"; +"room_event_action_save" = "حِفظ"; +"room_event_action_report_prompt_ignore_user" = "هَل تُريدُ إخفَاءَ جَميعِ الرَّسَائِلِ مِن هَذَا المُستَخدِم؟"; +"room_event_action_ban_prompt_reason" = "سَبَبُ حَظْرِ هَذَا المُستَخدِم"; +"room_event_action_kick_prompt_reason" = "سَبَبُ طَردِ هَذَا المُستَخدِم"; +"room_event_action_report_prompt_reason" = "سَبَبُ الإبلَاغِ عَن هَذَا المُحتَوى"; +"room_event_action_report" = "التَّبلِيغُ عَنِ المُحتَوَى"; +"room_event_action_view_decrypted_source" = "الاِطِّلاعُ عَلَى المَصدَرِ مَفكُوكِ التَّعميَة"; +"room_event_action_view_source" = "الاِطِّلاعُ عَلَى المَصدَر"; +"room_event_action_permalink" = "رَابِطٌ دَائِم"; +"room_event_action_share" = "مُشارَكَة"; +"room_event_action_more" = "مَزيد"; +"room_event_action_redact" = "إزالَة"; +"room_event_action_quote" = "اِقتِبَاس"; +"room_event_action_copy" = "نَسخ"; +"room_delete_unsent_messages" = "حَذفُ الرَّسائِلِ غَيرِ المُرسَلَة"; +"room_resend_unsent_messages" = "إعادَةُ إرسَالِ الرَّسائِلِ غَيرِ المُرسَلَة"; +"room_prompt_cancel" = "إلغَاءُ الكُل"; +"room_prompt_resend" = "إعادَةُ إرسَالِ الكُل"; +"room_conference_call_no_power" = "تَحتَاجُ إلَى إذنٍ لِإدَارَةِ مُكالَمَةِ اِجتِمَاعٍ فِي هَذِهِ الغُرفَة"; +"room_ongoing_conference_call_with_close" = "مُكالَمَةُ اِجتِمَاعٍ مُستَمِرَّة. الاِنضِمَامُ كَـ%@ أَو %@. %@ها."; +"room_ongoing_conference_call_close" = "إِغلَاق"; +"room_ongoing_conference_call" = "مُكالَمَةُ اِجتِمَاعٍ مُستَمِرَّة. الاِنضِمَامُ كَـ%@ أَو %@."; +"room_unsent_messages_cancel_message" = "هَل أنتَ مُتَأكِّدٌ أنَّكَ تٌريدُ حَذفَ جَميعِ الرَّسائِلِ غَيرِ المُرسَلَةِ فِي هَذِهِ الغُرفَة؟"; +"room_unsent_messages_cancel_title" = "حَذفُ الرَّسائِلِ غَيرِ المُرسَلَة"; +"room_unsent_messages_unknown_devices_notification" = "فَشَلَ إرسَالُ الرَّسائِلِ بِسَبَبِ جَلسَاتٍ حَاليةٍ غَيرِ مَعرُوفَة."; +"room_unsent_messages_notification" = "فَشَلَ إرسَالُ الرَّسائِل."; +"room_offline_notification" = "فُقِدَ الاِتِّصَالُ بِالخَادِم."; +"room_message_reply_to_short_placeholder" = "إرسَالُ رَدّ…"; +"room_message_short_placeholder" = "إرسَالُ رِسَالَة…"; +"room_participants_security_information_room_encrypted_for_dm" = "الرَّسائِلُ هُنا مُعمَاةٌ مِنَ النِّهايَةِ إلَى النِّهايَةِ. \n\nيَتِمُ تَأمينُ رَسائِلَكَ بِأقفَال، فَقَط أنتَ وَالمُستَلِمُ مَن لَديهِم مِفتَاحَينِ فَريدَينِ لِفتحِهَا."; +"room_participants_security_information_room_encrypted" = "الرَّسائِلُ فِي هَذِهِ الغُرفَة مُعمَاةٌ مِنَ النِّهايَةِ إلَى النِّهايَةِ. \n\nيَتِمُ تَأمينُ رَسائِلَكَ بِأقفَال، فَقَط أنتَ وَالمُستَلِمُ مَن لَديهِم مِفتَاحَينِ فَريدَينِ لِفتحِهَا."; +"room_participants_security_information_room_not_encrypted_for_dm" = "الرَّسائِلُ هُنا لَيسَت مُعمَاةً مِنَ النِّهايَةِ إلَى النِّهايَةِ."; +"room_participants_security_information_room_not_encrypted" = "الرَّسائِلُ فِي هَذِهِ الغُرفَة لَيسَت مُعمَاةً مِنَ النِّهايَةِ إلَى النِّهايَةِ."; +"encrypted_room_message_reply_to_placeholder" = "إرسَالُ رَدٍّ مُعمَى…"; +"encrypted_room_message_placeholder" = "إرسَالُ رِسَالَةٍ مُعمَاة…"; +"room_do_not_have_permission_to_post" = "لَيسَ لَديكَ إذنٌ بِالنَّشرِ فِي هَذِهِ الغُرفَة"; +"room_message_editing" = "التَّحرِير"; +"room_message_replying_to" = "الرَّدُّ عَلَى %@"; +"room_message_unable_open_link_error_message" = "يَتَعَذَّرُ فَتحُ الرَّابِط."; +"room_message_reply_to_placeholder" = "إرسَالُ رَدّ (غَيرُ مُعَمَى)…"; +"room_message_placeholder" = "إرسَالُ رِسَالَة (غَيرُ مُعَمَاة)…"; +"room_many_users_are_typing" = "إنَّ %@، %@ وَآخَرُونَ يَكتُبُون…"; +"room_two_users_are_typing" = "إنَّ %@ وَ %@ يَكتُبَان…"; +"room_one_user_is_typing" = "إنَّ %@ يَكتُب…"; +"room_new_messages_notification" = "عَدَدُ %d رَسَائِلَ جَدِيدَة"; +"room_new_message_notification" = "عَدَدُ %d رِسَالَةً جَدِيدَة"; +"room_accessiblity_scroll_to_bottom" = "التَّمرِيرُ إلَى الأَسفَل"; +"room_jump_to_first_unread" = "القَفزُ إلَى غَيرِ المَقرُوء"; + +// Chat +"room_slide_to_end_group_call" = "مَرِّرِ لِإنهَاءِ المُكالَمَةِ لِلجَّميع"; +"room_member_power_level_short_custom" = "مُتَخَصِّص"; +"room_member_power_level_short_moderator" = "مُشرِف"; +"room_member_power_level_short_admin" = "مُدير"; +"room_member_power_level_custom_in" = "مُتَخَصِّصٌ (%@) في %@"; +"room_member_power_level_moderator_in" = "مُشرِفٌ فِي %@"; +"room_member_power_level_admin_in" = "مُديرٌ فِي %@"; +"room_participants_security_loading" = "التَّحمِيلُ جَارٍ…"; +"room_participants_action_security_status_loading" = "التَّحمِيلُ جَارٍ…"; +"room_participants_action_security_status_warning" = "تَحذِير"; +"room_participants_action_security_status_complete_security" = "الأمَانُ الكَامِل"; +"room_participants_action_security_status_verify" = "تَأكِيدُ التَّحَقُّق"; +"room_participants_action_security_status_verified" = "مُتَحَقَّقٌ مِنه"; +"room_participants_action_mention" = "ذِكْر"; +"room_participants_action_start_video_call" = "بَدْءُ مُكالَمَةٍ مَرئيَّة"; +"room_participants_action_start_voice_call" = "بَدْءُ مُكالَمَةٍ صَوتيَّة"; +"room_participants_action_start_new_chat" = "بَدْءُ مُحادَثَةٍ جَدِيدَة"; +"room_participants_action_set_admin" = "جَعْلَهُ مُدِير"; +"room_participants_action_set_moderator" = "جَعْلَهُ مُشرِف"; +"room_participants_action_set_default_power_level" = "إعَادَةُ الضَّبطِ إلَى مُستَخدِمٍ عَادِيّ"; +"room_participants_action_unignore" = "إِظْهَارُ جَمِيعِ الرَّسائِلِ مِن هَذَا المُستَخدِم"; +"room_participants_action_ignore" = "إخفَاءُ جَمِيعِ الرَّسائِلِ مِن هَذَا المُستَخدِم"; +"room_recents_unknown_room_error_message" = "يَتَعَذَّر العُثُور عَلَى هَذِهِ الغُرفَة. تأكَّد مِن وجودِهَا"; +"room_creation_dm_error" = "يَتَعَذَّرُ عَلينَا إنشَاء المُحادَثَة المُباشِرَة الخَّاصَةِ بِك. يُرجَى التَّحَقُقُ مِنَ المُستَخدِمِيَنَ اللَّذِينَ تُريدُ دَعوَتَهُم ثُمَّ المُحاوَلَةُ مَرةً أُخرَى."; From 823041862f13e25db2515a92b3cc3d84d88313d8 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 22 Jul 2021 11:17:37 +0200 Subject: [PATCH 92/96] version++ --- CHANGES.rst | 5 ++++- Podfile | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index be5868916..7bb3eb350 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -Changes to be released in next version +Changes in 1.4.7 (2021-07-22) ================================================= ✨ Features @@ -22,6 +22,9 @@ Changes to be released in next version Others * +Improvements: + * Upgrade MatrixKit version ([v0.15.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.15.5)). + Changes in 1.4.6 (2021-07-16) ================================================= diff --git a/Podfile b/Podfile index 2c4471394..d9a65728f 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.15.4' +$matrixKitVersion = '= 0.15.5' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} From e0ab1faf39b9030c1dcadfd1be536a520efb7009 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 22 Jul 2021 12:34:13 +0200 Subject: [PATCH 93/96] finish version++ --- Podfile.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 8b171f979..caf60feb9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -56,29 +56,29 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.15.4): + - MatrixKit (0.15.5): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.15.4) - - MatrixSDK (= 0.19.4) - - MatrixKit/Core (0.15.4): + - MatrixKit/Core (= 0.15.5) + - MatrixSDK (= 0.19.5) + - MatrixKit/Core (0.15.5): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.19.4) - - MatrixSDK (0.19.4): - - MatrixSDK/Core (= 0.19.4) - - MatrixSDK/Core (0.19.4): + - MatrixSDK (= 0.19.5) + - MatrixSDK (0.19.5): + - MatrixSDK/Core (= 0.19.5) + - MatrixSDK/Core (0.19.5): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.4) - Realm (= 10.7.6) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.19.4): + - MatrixSDK/JingleCallStack (0.19.5): - JitsiMeetSDK (= 3.5.0) - MatrixSDK/Core - OLMKit (3.2.4): @@ -120,7 +120,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.15.4) + - MatrixKit (= 0.15.5) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -196,8 +196,8 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: 477690d6dd38b1526042f8e83ff34e3db87e3a00 - MatrixSDK: 0c9d1845a45d71abcfcabe82b706cadbda1297ae + MatrixKit: 7606227237cf58c1a1a2235547222c5d75b464c4 + MatrixSDK: 9fa30f9ca2504c4251b99212dcf4ff569bbf45b1 OLMKit: 2d73cd67d149b5c3e3a8eb8ecae93d0b429d8a02 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: ed860452717c8db8f4bf832b6807f7f2ce708839 @@ -211,6 +211,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 2b61b852a5b62fbb75643a30281e362fd4566f12 +PODFILE CHECKSUM: 6e4ab90565384faccee0cb985abe05663c36f517 COCOAPODS: 1.10.1 From a25a50e60e88d029e562536e1aa07e0942893bb3 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 22 Jul 2021 12:36:29 +0200 Subject: [PATCH 94/96] finish version++ From 36f5b61169ce10ecb2385ba044d8716e1416f4cd Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 22 Jul 2021 12:36:33 +0200 Subject: [PATCH 95/96] 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 96/96] 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