Integrate new voice message design
This commit is contained in:
parent
6ff0834065
commit
aa568aba7b
|
@ -571,6 +571,7 @@
|
|||
C31D1DDD25217014005D4DA8 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; };
|
||||
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
|
||||
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; };
|
||||
C31F8117252546F200DD9FD9 /* file_example_MP3_2MG.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */; };
|
||||
C329FEEC24F7277900B1C64C /* LightModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEEB24F7277900B1C64C /* LightModeSheet.swift */; };
|
||||
C329FEEF24F7743F00B1C64C /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */; };
|
||||
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; };
|
||||
|
@ -1370,6 +1371,7 @@
|
|||
C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; };
|
||||
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
|
||||
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
|
||||
C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = file_example_MP3_2MG.mp3; sourceTree = "<group>"; };
|
||||
C329FEEB24F7277900B1C64C /* LightModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightModeSheet.swift; sourceTree = "<group>"; };
|
||||
C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utilities.swift"; sourceTree = "<group>"; };
|
||||
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
|
||||
|
@ -2870,6 +2872,7 @@
|
|||
C35E8AA42485C83B00ACB629 /* CSV */,
|
||||
34330A581E7875FB00DF2FB9 /* Fonts */,
|
||||
B633C4FD1A1D190B0059AC12 /* Images */,
|
||||
C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */,
|
||||
B66DBF4919D5BBC8006EA940 /* Images.xcassets */,
|
||||
B67EBF5C19194AC60084CCFD /* Settings.bundle */,
|
||||
B657DDC91911A40500F45B0C /* Signal.entitlements */,
|
||||
|
@ -3263,6 +3266,7 @@
|
|||
AD83FF411A73426500B5C81A /* audio_play_button_blue@2x.png in Resources */,
|
||||
34C3C78D20409F320000134C /* Opening.m4r in Resources */,
|
||||
FC5CDF3A1A3393DD00B47253 /* warning_white@2x.png in Resources */,
|
||||
C31F8117252546F200DD9FD9 /* file_example_MP3_2MG.mp3 in Resources */,
|
||||
B633C58D1A1D190B0059AC12 /* contact_default_feed.png in Resources */,
|
||||
B10C9B621A7049EC00ECA2BF /* play_icon@2x.png in Resources */,
|
||||
B633C5861A1D190B0059AC12 /* call@2x.png in Resources */,
|
||||
|
|
Binary file not shown.
|
@ -2,17 +2,16 @@ import Accelerate
|
|||
|
||||
@objc(LKVoiceMessageView2)
|
||||
final class VoiceMessageView2 : UIView {
|
||||
private let audioFileURL: URL
|
||||
private let player: AVAudioPlayer
|
||||
private var duration: Double = 1
|
||||
private let voiceMessage: TSAttachment
|
||||
private var isAnimating = false
|
||||
private var volumeSamples: [Float] = [] { didSet { updateShapeLayer() } }
|
||||
private var volumeSamples: [Float] = [] { didSet { updateShapeLayers() } }
|
||||
private var progress: CGFloat = 0
|
||||
private var duration: CGFloat = 1 // Not initialized at 0 to avoid division by zero
|
||||
|
||||
// MARK: Components
|
||||
private lazy var loader: UIView = {
|
||||
let result = UIView()
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(0.2)
|
||||
result.layer.cornerRadius = Values.messageBubbleCornerRadius
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -29,34 +28,41 @@ final class VoiceMessageView2 : UIView {
|
|||
}()
|
||||
|
||||
// MARK: Settings
|
||||
private let margin = Values.smallSpacing
|
||||
private let margin: CGFloat = 4
|
||||
private let sampleSpacing: CGFloat = 1
|
||||
|
||||
// MARK: Initialization
|
||||
init(audioFileURL: URL) {
|
||||
self.audioFileURL = audioFileURL
|
||||
player = try! AVAudioPlayer(contentsOf: audioFileURL)
|
||||
@objc(initWithVoiceMessage:)
|
||||
init(voiceMessage: TSAttachment) {
|
||||
self.voiceMessage = voiceMessage
|
||||
super.init(frame: CGRect.zero)
|
||||
initialize()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(audioFileURL:) instead.")
|
||||
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(audioFileURL:) instead.")
|
||||
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
|
||||
}
|
||||
|
||||
private func initialize() {
|
||||
setUpViewHierarchy()
|
||||
AudioUtilities.getVolumeSamples(for: audioFileURL).done(on: DispatchQueue.main) { [weak self] duration, volumeSamples in
|
||||
guard let self = self else { return }
|
||||
self.duration = duration
|
||||
self.volumeSamples = volumeSamples
|
||||
self.stopAnimating()
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
print("[Loki] Couldn't sample audio file due to error: \(error).")
|
||||
if voiceMessage.isDownloaded {
|
||||
loader.alpha = 0
|
||||
guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
|
||||
return print("[Loki] Couldn't get URL for voice message.")
|
||||
}
|
||||
AudioUtilities.getVolumeSamples(for: url).done(on: DispatchQueue.main) { [weak self] volumeSamples in
|
||||
guard let self = self else { return }
|
||||
self.volumeSamples = volumeSamples
|
||||
self.stopAnimating()
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
print("[Loki] Couldn't sample audio file due to error: \(error).")
|
||||
}
|
||||
} else {
|
||||
showLoader()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,16 +71,11 @@ final class VoiceMessageView2 : UIView {
|
|||
set(.height, to: 40)
|
||||
addSubview(loader)
|
||||
loader.pin(to: self)
|
||||
backgroundColor = Colors.sentMessageBackground
|
||||
layer.cornerRadius = Values.messageBubbleCornerRadius
|
||||
layer.insertSublayer(backgroundShapeLayer, at: 0)
|
||||
layer.insertSublayer(foregroundShapeLayer, at: 1)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(togglePlayback))
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
showLoader()
|
||||
}
|
||||
|
||||
// MARK: User Interface
|
||||
// MARK: UI & Updating
|
||||
private func showLoader() {
|
||||
isAnimating = true
|
||||
loader.alpha = 1
|
||||
|
@ -98,10 +99,17 @@ final class VoiceMessageView2 : UIView {
|
|||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateShapeLayer()
|
||||
updateShapeLayers()
|
||||
}
|
||||
|
||||
private func updateShapeLayer() {
|
||||
@objc(updateForProgress:duration:)
|
||||
func update(for progress: CGFloat, duration: CGFloat) {
|
||||
self.progress = progress
|
||||
self.duration = duration
|
||||
updateShapeLayers()
|
||||
}
|
||||
|
||||
private func updateShapeLayers() {
|
||||
guard !volumeSamples.isEmpty else { return }
|
||||
let max = CGFloat(volumeSamples.max()!)
|
||||
let min = CGFloat(volumeSamples.min()!)
|
||||
|
@ -117,20 +125,11 @@ final class VoiceMessageView2 : UIView {
|
|||
let y = margin + (h - sH) / 2
|
||||
let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2)
|
||||
backgroundPath.append(subPath)
|
||||
if player.currentTime / duration > Double(i) / Double(volumeSamples.count) { foregroundPath.append(subPath) }
|
||||
if progress / duration > CGFloat(i) / CGFloat(volumeSamples.count) { foregroundPath.append(subPath) }
|
||||
}
|
||||
backgroundPath.close()
|
||||
foregroundPath.close()
|
||||
backgroundShapeLayer.path = backgroundPath.cgPath
|
||||
foregroundShapeLayer.path = foregroundPath.cgPath
|
||||
}
|
||||
|
||||
@objc private func togglePlayback() {
|
||||
player.play()
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
||||
guard let self = self else { return timer.invalidate() }
|
||||
self.updateShapeLayer()
|
||||
if !self.player.isPlaying { timer.invalidate() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ enum AudioUtilities {
|
|||
}
|
||||
}
|
||||
|
||||
static func getVolumeSamples(for audioFileURL: URL, targetSampleCount: Int = 32) -> Promise<(duration: Double, volumeSamples: [Float])> {
|
||||
static func getVolumeSamples(for audioFileURL: URL, targetSampleCount: Int = 32) -> Promise<[Float]> {
|
||||
return loadFile(audioFileURL).then { fileInfo in
|
||||
AudioUtilities.parseSamples(from: fileInfo, with: targetSampleCount)
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ enum AudioUtilities {
|
|||
return promise
|
||||
}
|
||||
|
||||
private static func parseSamples(from fileInfo: FileInfo, with targetSampleCount: Int) -> Promise<(duration: Double, volumeSamples: [Float])> {
|
||||
private static func parseSamples(from fileInfo: FileInfo, with targetSampleCount: Int) -> Promise<[Float]> {
|
||||
// Prepare the reader
|
||||
guard let reader = try? AVAssetReader(asset: fileInfo.asset) else { return Promise(error: Error.parsingFailed) }
|
||||
let range = 0..<fileInfo.sampleCount
|
||||
|
@ -128,8 +128,7 @@ enum AudioUtilities {
|
|||
}
|
||||
guard reader.status == .completed else { return Promise(error: Error.parsingFailed) }
|
||||
// Return
|
||||
let duration = fileInfo.asset.duration.seconds
|
||||
return Promise { $0.fulfill((duration, result)) }
|
||||
return Promise { $0.fulfill(result) }
|
||||
}
|
||||
|
||||
private static func processSamples(from sampleBuffer: inout Data, outputSamples: inout [Float], samplesToProcess: Int,
|
||||
|
|
|
@ -839,10 +839,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(attachment);
|
||||
OWSAssertDebug([attachment isAudio]);
|
||||
|
||||
LKVoiceMessageView *voiceMessageView = [[LKVoiceMessageView alloc] initWithVoiceMessage:attachment viewItem:self.viewItem];
|
||||
LKVoiceMessageView2 *voiceMessageView = [[LKVoiceMessageView2 alloc] initWithVoiceMessage:attachment];
|
||||
|
||||
self.viewItem.lastAudioMessageView = voiceMessageView;
|
||||
[voiceMessageView update];
|
||||
|
||||
self.loadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
|
@ -1064,7 +1063,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return nil;
|
||||
}
|
||||
case OWSMessageCellType_Audio:
|
||||
result = CGSizeMake(maxMessageWidth, [LKVoiceMessageView getHeightFor:self.viewItem]);
|
||||
result = CGSizeMake(maxMessageWidth, 40.0f);
|
||||
break;
|
||||
case OWSMessageCellType_GenericAttachment: {
|
||||
TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer);
|
||||
|
|
|
@ -24,7 +24,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
@class ContactShareViewModel;
|
||||
@class ConversationViewCell;
|
||||
@class DisplayableText;
|
||||
@class LKVoiceMessageView;
|
||||
@class LKVoiceMessageView2;
|
||||
@class OWSLinkPreview;
|
||||
@class OWSQuotedReplyModel;
|
||||
@class OWSUnreadIndicator;
|
||||
|
@ -99,7 +99,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
|
||||
#pragma mark - Audio Playback
|
||||
|
||||
@property (nonatomic, weak) LKVoiceMessageView *lastAudioMessageView;
|
||||
@property (nonatomic, weak) LKVoiceMessageView2 *lastAudioMessageView;
|
||||
|
||||
@property (nonatomic, readonly) CGFloat audioDurationSeconds;
|
||||
@property (nonatomic, readonly) CGFloat audioProgressSeconds;
|
||||
|
|
|
@ -475,7 +475,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
{
|
||||
_audioPlaybackState = audioPlaybackState;
|
||||
|
||||
[self.lastAudioMessageView update];
|
||||
// No need to update the voice message view here
|
||||
}
|
||||
|
||||
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration
|
||||
|
@ -484,7 +484,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
self.audioProgressSeconds = progress;
|
||||
|
||||
[self.lastAudioMessageView update];
|
||||
[self.lastAudioMessageView updateForProgress:progress duration:duration];
|
||||
}
|
||||
|
||||
#pragma mark - Displayable Text
|
||||
|
|
|
@ -146,7 +146,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
[self.audioPlayer play];
|
||||
[self.audioPlayerPoller invalidate];
|
||||
self.audioPlayerPoller = [NSTimer weakScheduledTimerWithTimeInterval:.05f
|
||||
self.audioPlayerPoller = [NSTimer weakScheduledTimerWithTimeInterval:.5f
|
||||
target:self
|
||||
selector:@selector(audioPlayerUpdated:)
|
||||
userInfo:nil
|
||||
|
|
Loading…
Reference in New Issue