Integrate new voice message design

pull/288/head
nielsandriesse 5 years ago
parent 6ff0834065
commit aa568aba7b

@ -571,6 +571,7 @@
C31D1DDD25217014005D4DA8 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; }; C31D1DDD25217014005D4DA8 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; };
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.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 */; }; C329FEEC24F7277900B1C64C /* LightModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEEB24F7277900B1C64C /* LightModeSheet.swift */; };
C329FEEF24F7743F00B1C64C /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEED24F7742E00B1C64C /* UIViewController+Utilities.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
@ -2870,6 +2872,7 @@
C35E8AA42485C83B00ACB629 /* CSV */, C35E8AA42485C83B00ACB629 /* CSV */,
34330A581E7875FB00DF2FB9 /* Fonts */, 34330A581E7875FB00DF2FB9 /* Fonts */,
B633C4FD1A1D190B0059AC12 /* Images */, B633C4FD1A1D190B0059AC12 /* Images */,
C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */,
B66DBF4919D5BBC8006EA940 /* Images.xcassets */, B66DBF4919D5BBC8006EA940 /* Images.xcassets */,
B67EBF5C19194AC60084CCFD /* Settings.bundle */, B67EBF5C19194AC60084CCFD /* Settings.bundle */,
B657DDC91911A40500F45B0C /* Signal.entitlements */, B657DDC91911A40500F45B0C /* Signal.entitlements */,
@ -3263,6 +3266,7 @@
AD83FF411A73426500B5C81A /* audio_play_button_blue@2x.png in Resources */, AD83FF411A73426500B5C81A /* audio_play_button_blue@2x.png in Resources */,
34C3C78D20409F320000134C /* Opening.m4r in Resources */, 34C3C78D20409F320000134C /* Opening.m4r in Resources */,
FC5CDF3A1A3393DD00B47253 /* warning_white@2x.png in Resources */, FC5CDF3A1A3393DD00B47253 /* warning_white@2x.png in Resources */,
C31F8117252546F200DD9FD9 /* file_example_MP3_2MG.mp3 in Resources */,
B633C58D1A1D190B0059AC12 /* contact_default_feed.png in Resources */, B633C58D1A1D190B0059AC12 /* contact_default_feed.png in Resources */,
B10C9B621A7049EC00ECA2BF /* play_icon@2x.png in Resources */, B10C9B621A7049EC00ECA2BF /* play_icon@2x.png in Resources */,
B633C5861A1D190B0059AC12 /* call@2x.png in Resources */, B633C5861A1D190B0059AC12 /* call@2x.png in Resources */,

Binary file not shown.

@ -2,17 +2,16 @@ import Accelerate
@objc(LKVoiceMessageView2) @objc(LKVoiceMessageView2)
final class VoiceMessageView2 : UIView { final class VoiceMessageView2 : UIView {
private let audioFileURL: URL private let voiceMessage: TSAttachment
private let player: AVAudioPlayer
private var duration: Double = 1
private var isAnimating = false 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 // MARK: Components
private lazy var loader: UIView = { private lazy var loader: UIView = {
let result = UIView() let result = UIView()
result.backgroundColor = Colors.text.withAlphaComponent(0.2) result.backgroundColor = Colors.text.withAlphaComponent(0.2)
result.layer.cornerRadius = Values.messageBubbleCornerRadius
return result return result
}() }()
@ -29,34 +28,41 @@ final class VoiceMessageView2 : UIView {
}() }()
// MARK: Settings // MARK: Settings
private let margin = Values.smallSpacing private let margin: CGFloat = 4
private let sampleSpacing: CGFloat = 1 private let sampleSpacing: CGFloat = 1
// MARK: Initialization // MARK: Initialization
init(audioFileURL: URL) { @objc(initWithVoiceMessage:)
self.audioFileURL = audioFileURL init(voiceMessage: TSAttachment) {
player = try! AVAudioPlayer(contentsOf: audioFileURL) self.voiceMessage = voiceMessage
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
initialize() initialize()
} }
override init(frame: CGRect) { override init(frame: CGRect) {
preconditionFailure("Use init(audioFileURL:) instead.") preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
preconditionFailure("Use init(audioFileURL:) instead.") preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
} }
private func initialize() { private func initialize() {
setUpViewHierarchy() setUpViewHierarchy()
AudioUtilities.getVolumeSamples(for: audioFileURL).done(on: DispatchQueue.main) { [weak self] duration, volumeSamples in if voiceMessage.isDownloaded {
guard let self = self else { return } loader.alpha = 0
self.duration = duration guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
self.volumeSamples = volumeSamples return print("[Loki] Couldn't get URL for voice message.")
self.stopAnimating() }
}.catch(on: DispatchQueue.main) { error in AudioUtilities.getVolumeSamples(for: url).done(on: DispatchQueue.main) { [weak self] volumeSamples in
print("[Loki] Couldn't sample audio file due to error: \(error).") 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) set(.height, to: 40)
addSubview(loader) addSubview(loader)
loader.pin(to: self) loader.pin(to: self)
backgroundColor = Colors.sentMessageBackground
layer.cornerRadius = Values.messageBubbleCornerRadius
layer.insertSublayer(backgroundShapeLayer, at: 0) layer.insertSublayer(backgroundShapeLayer, at: 0)
layer.insertSublayer(foregroundShapeLayer, at: 1) 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() { private func showLoader() {
isAnimating = true isAnimating = true
loader.alpha = 1 loader.alpha = 1
@ -98,10 +99,17 @@ final class VoiceMessageView2 : UIView {
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
updateShapeLayer() updateShapeLayers()
}
@objc(updateForProgress:duration:)
func update(for progress: CGFloat, duration: CGFloat) {
self.progress = progress
self.duration = duration
updateShapeLayers()
} }
private func updateShapeLayer() { private func updateShapeLayers() {
guard !volumeSamples.isEmpty else { return } guard !volumeSamples.isEmpty else { return }
let max = CGFloat(volumeSamples.max()!) let max = CGFloat(volumeSamples.max()!)
let min = CGFloat(volumeSamples.min()!) let min = CGFloat(volumeSamples.min()!)
@ -117,20 +125,11 @@ final class VoiceMessageView2 : UIView {
let y = margin + (h - sH) / 2 let y = margin + (h - sH) / 2
let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2) let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2)
backgroundPath.append(subPath) 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() backgroundPath.close()
foregroundPath.close() foregroundPath.close()
backgroundShapeLayer.path = backgroundPath.cgPath backgroundShapeLayer.path = backgroundPath.cgPath
foregroundShapeLayer.path = foregroundPath.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 return loadFile(audioFileURL).then { fileInfo in
AudioUtilities.parseSamples(from: fileInfo, with: targetSampleCount) AudioUtilities.parseSamples(from: fileInfo, with: targetSampleCount)
} }
@ -59,7 +59,7 @@ enum AudioUtilities {
return promise 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 // Prepare the reader
guard let reader = try? AVAssetReader(asset: fileInfo.asset) else { return Promise(error: Error.parsingFailed) } guard let reader = try? AVAssetReader(asset: fileInfo.asset) else { return Promise(error: Error.parsingFailed) }
let range = 0..<fileInfo.sampleCount let range = 0..<fileInfo.sampleCount
@ -128,8 +128,7 @@ enum AudioUtilities {
} }
guard reader.status == .completed else { return Promise(error: Error.parsingFailed) } guard reader.status == .completed else { return Promise(error: Error.parsingFailed) }
// Return // Return
let duration = fileInfo.asset.duration.seconds return Promise { $0.fulfill(result) }
return Promise { $0.fulfill((duration, result)) }
} }
private static func processSamples(from sampleBuffer: inout Data, outputSamples: inout [Float], samplesToProcess: Int, 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);
OWSAssertDebug([attachment isAudio]); OWSAssertDebug([attachment isAudio]);
LKVoiceMessageView *voiceMessageView = [[LKVoiceMessageView alloc] initWithVoiceMessage:attachment viewItem:self.viewItem]; LKVoiceMessageView2 *voiceMessageView = [[LKVoiceMessageView2 alloc] initWithVoiceMessage:attachment];
self.viewItem.lastAudioMessageView = voiceMessageView; self.viewItem.lastAudioMessageView = voiceMessageView;
[voiceMessageView update];
self.loadCellContentBlock = ^{ self.loadCellContentBlock = ^{
// Do nothing. // Do nothing.
@ -1064,7 +1063,7 @@ NS_ASSUME_NONNULL_BEGIN
return nil; return nil;
} }
case OWSMessageCellType_Audio: case OWSMessageCellType_Audio:
result = CGSizeMake(maxMessageWidth, [LKVoiceMessageView getHeightFor:self.viewItem]); result = CGSizeMake(maxMessageWidth, 40.0f);
break; break;
case OWSMessageCellType_GenericAttachment: { case OWSMessageCellType_GenericAttachment: {
TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer); TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer);

@ -24,7 +24,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@class ContactShareViewModel; @class ContactShareViewModel;
@class ConversationViewCell; @class ConversationViewCell;
@class DisplayableText; @class DisplayableText;
@class LKVoiceMessageView; @class LKVoiceMessageView2;
@class OWSLinkPreview; @class OWSLinkPreview;
@class OWSQuotedReplyModel; @class OWSQuotedReplyModel;
@class OWSUnreadIndicator; @class OWSUnreadIndicator;
@ -99,7 +99,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
#pragma mark - Audio Playback #pragma mark - Audio Playback
@property (nonatomic, weak) LKVoiceMessageView *lastAudioMessageView; @property (nonatomic, weak) LKVoiceMessageView2 *lastAudioMessageView;
@property (nonatomic, readonly) CGFloat audioDurationSeconds; @property (nonatomic, readonly) CGFloat audioDurationSeconds;
@property (nonatomic, readonly) CGFloat audioProgressSeconds; @property (nonatomic, readonly) CGFloat audioProgressSeconds;

@ -475,7 +475,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
{ {
_audioPlaybackState = audioPlaybackState; _audioPlaybackState = audioPlaybackState;
[self.lastAudioMessageView update]; // No need to update the voice message view here
} }
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration - (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration
@ -484,7 +484,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
self.audioProgressSeconds = progress; self.audioProgressSeconds = progress;
[self.lastAudioMessageView update]; [self.lastAudioMessageView updateForProgress:progress duration:duration];
} }
#pragma mark - Displayable Text #pragma mark - Displayable Text

@ -146,7 +146,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.audioPlayer play]; [self.audioPlayer play];
[self.audioPlayerPoller invalidate]; [self.audioPlayerPoller invalidate];
self.audioPlayerPoller = [NSTimer weakScheduledTimerWithTimeInterval:.05f self.audioPlayerPoller = [NSTimer weakScheduledTimerWithTimeInterval:.5f
target:self target:self
selector:@selector(audioPlayerUpdated:) selector:@selector(audioPlayerUpdated:)
userInfo:nil userInfo:nil

Loading…
Cancel
Save