瀏覽代碼

inital audio cells implementation, heavily based on message kit chat example project

cyberta 5 年之前
父節點
當前提交
2f63cf9c21
共有 32 個文件被更改,包括 417 次插入31 次删除
  1. 二進制
      Assets.xcassets/disclouser.png
  2. 二進制
      Assets.xcassets/disclouser@2x.png
  3. 二進制
      Assets.xcassets/disclouser@3x.png
  4. 二進制
      Assets.xcassets/pause.png
  5. 二進制
      Assets.xcassets/pause@2x.png
  6. 二進制
      Assets.xcassets/pause@3x.png
  7. 二進制
      Assets.xcassets/play.png
  8. 二進制
      Assets.xcassets/play@2x.png
  9. 二進制
      Assets.xcassets/play@3x.png
  10. 8 0
      deltachat-ios.xcodeproj/project.pbxproj
  11. 23 0
      deltachat-ios/Assets.xcassets/disclouser.imageset/Contents.json
  12. 二進制
      deltachat-ios/Assets.xcassets/disclouser.imageset/disclouser.png
  13. 二進制
      deltachat-ios/Assets.xcassets/disclouser.imageset/disclouser@2x.png
  14. 二進制
      deltachat-ios/Assets.xcassets/disclouser.imageset/disclouser@3x.png
  15. 6 0
      deltachat-ios/Assets.xcassets/ic_attach_file_black_36dp/Contents.json
  16. 23 0
      deltachat-ios/Assets.xcassets/pause.imageset/Contents.json
  17. 二進制
      deltachat-ios/Assets.xcassets/pause.imageset/pause.png
  18. 二進制
      deltachat-ios/Assets.xcassets/pause.imageset/pause@2x.png
  19. 二進制
      deltachat-ios/Assets.xcassets/pause.imageset/pause@3x.png
  20. 23 0
      deltachat-ios/Assets.xcassets/play.imageset/Contents.json
  21. 二進制
      deltachat-ios/Assets.xcassets/play.imageset/play.png
  22. 二進制
      deltachat-ios/Assets.xcassets/play.imageset/play@2x.png
  23. 二進制
      deltachat-ios/Assets.xcassets/play.imageset/play@3x.png
  24. 44 0
      deltachat-ios/Controller/ChatViewController.swift
  25. 47 20
      deltachat-ios/DC/Wrapper.swift
  26. 0 7
      deltachat-ios/Extensions/UIImage+Extension.swift
  27. 5 0
      deltachat-ios/Helper/Utils.swift
  28. 213 0
      deltachat-ios/MessageKit/Controllers/BasicAudioController.swift
  29. 2 0
      deltachat-ios/MessageKit/Protocols/AudioItem.swift
  30. 3 3
      deltachat-ios/MessageKit/Views/Cells/AudioMessageCell.swift
  31. 1 1
      deltachat-ios/MessageKit/Views/Cells/ContactMessageCell.swift
  32. 19 0
      deltachat-ios/Model/Audio.swift

二進制
Assets.xcassets/disclouser.png


二進制
Assets.xcassets/disclouser@2x.png


二進制
Assets.xcassets/disclouser@3x.png


二進制
Assets.xcassets/pause.png


二進制
Assets.xcassets/pause@2x.png


二進制
Assets.xcassets/pause@3x.png


二進制
Assets.xcassets/play.png


二進制
Assets.xcassets/play@2x.png


二進制
Assets.xcassets/play@3x.png


+ 8 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -11,6 +11,8 @@
 		300C50A1234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300C50A0234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift */; };
 		30149D9322F21129003C12B5 /* QrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30149D9222F21129003C12B5 /* QrViewController.swift */; };
 		3022E6BE22E8768800763272 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3022E6C022E8768800763272 /* InfoPlist.strings */; };
+		3040F45E234DFBC000FA34D5 /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3040F45D234DFBC000FA34D5 /* Audio.swift */; };
+		3040F460234F419400FA34D5 /* BasicAudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3040F45F234F419300FA34D5 /* BasicAudioController.swift */; };
 		305961CC2346125100C80F33 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961822346125000C80F33 /* UIView+Extensions.swift */; };
 		305961CD2346125100C80F33 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */; };
 		305961CF2346125100C80F33 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961852346125000C80F33 /* UIColor+Extensions.swift */; };
@@ -174,6 +176,8 @@
 		3022E6D122E8769E00763272 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6D222E8769F00763272 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
 		3022E6D322E876A100763272 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		3040F45D234DFBC000FA34D5 /* Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audio.swift; sourceTree = "<group>"; };
+		3040F45F234F419300FA34D5 /* BasicAudioController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicAudioController.swift; sourceTree = "<group>"; };
 		305961822346125000C80F33 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
 		305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = "<group>"; };
 		305961852346125000C80F33 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
@@ -410,6 +414,7 @@
 		305961892346125000C80F33 /* Controllers */ = {
 			isa = PBXGroup;
 			children = (
+				3040F45F234F419300FA34D5 /* BasicAudioController.swift */,
 				3059618A2346125000C80F33 /* MessagesViewController+Keyboard.swift */,
 				3059618B2346125000C80F33 /* MessagesViewController.swift */,
 				3059618C2346125000C80F33 /* MessagesViewController+Menu.swift */,
@@ -629,6 +634,7 @@
 				AEACE2DE1FB3246400DCDD78 /* Message.swift */,
 				AE851AC6227C776400ED86F0 /* Location.swift */,
 				AE851AC8227C77CF00ED86F0 /* Media.swift */,
+				3040F45D234DFBC000FA34D5 /* Audio.swift */,
 			);
 			path = Model;
 			sourceTree = "<group>";
@@ -1013,6 +1019,7 @@
 				305961FA2346125100C80F33 /* MessageReusableView.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
 				78E45E4421D3F14A00D4B15E /* UIImage+Extension.swift in Sources */,
+				3040F460234F419400FA34D5 /* BasicAudioController.swift in Sources */,
 				305962082346125100C80F33 /* MediaMessageSizeCalculator.swift in Sources */,
 				AE52EA20229EB9F000C586C9 /* EditGroupViewController.swift in Sources */,
 				70B08FCD21073B910097D3EA /* NewGroupMemberChoiceController.swift in Sources */,
@@ -1021,6 +1028,7 @@
 				78ED838D21D577D000243125 /* events.swift in Sources */,
 				305961FD2346125100C80F33 /* TypingBubble.swift in Sources */,
 				305961D72346125100C80F33 /* MessageKit+Availability.swift in Sources */,
+				3040F45E234DFBC000FA34D5 /* Audio.swift in Sources */,
 				305961FE2346125100C80F33 /* InsetLabel.swift in Sources */,
 				B21005DB23383664004C70C5 /* SettingsClassicViewController.swift in Sources */,
 				305961F62346125100C80F33 /* MessageContentCell.swift in Sources */,

+ 23 - 0
deltachat-ios/Assets.xcassets/disclouser.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "disclouser.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "disclouser@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "disclouser@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

二進制
deltachat-ios/Assets.xcassets/disclouser.imageset/disclouser.png


二進制
deltachat-ios/Assets.xcassets/disclouser.imageset/disclouser@2x.png


二進制
deltachat-ios/Assets.xcassets/disclouser.imageset/disclouser@3x.png


+ 6 - 0
deltachat-ios/Assets.xcassets/ic_attach_file_black_36dp/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 23 - 0
deltachat-ios/Assets.xcassets/pause.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "pause.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "pause@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "pause@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

二進制
deltachat-ios/Assets.xcassets/pause.imageset/pause.png


二進制
deltachat-ios/Assets.xcassets/pause.imageset/pause@2x.png


二進制
deltachat-ios/Assets.xcassets/pause.imageset/pause@3x.png


+ 23 - 0
deltachat-ios/Assets.xcassets/play.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "play.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "play@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "play@3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

二進制
deltachat-ios/Assets.xcassets/play.imageset/play.png


二進制
deltachat-ios/Assets.xcassets/play.imageset/play@2x.png


二進制
deltachat-ios/Assets.xcassets/play.imageset/play@3x.png


+ 44 - 0
deltachat-ios/Controller/ChatViewController.swift

@@ -2,6 +2,7 @@ import MapKit
 import QuickLook
 import UIKit
 import InputBarAccessoryView
+import AVFoundation
 
 protocol MediaSendHandler {
     func onSuccess()
@@ -31,6 +32,9 @@ class ChatViewController: MessagesViewController {
         UITapGestureRecognizer(target: self, action: #selector(chatProfilePressed))
     }()
 
+    /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and udpate audio cell UI accordingly.
+    open lazy var audioController = BasicAudioController(messageCollectionView: messagesCollectionView)
+
     var disableWriting = false
     var showCustomNavBar = true
     var previewView: UIView?
@@ -158,6 +162,7 @@ class ChatViewController: MessagesViewController {
         if let incomingMsgObserver = self.incomingMsgObserver {
             nc.removeObserver(incomingMsgObserver)
         }
+        audioController.stopAnyOngoingPlaying()
     }
 
     @objc
@@ -901,6 +906,45 @@ extension ChatViewController: MessageCellDelegate {
         print("Bottom label tapped")
     }
 
+    func didTapPlayButton(in cell: AudioMessageCell) {
+        guard let indexPath = messagesCollectionView.indexPath(for: cell),
+            let message = messagesCollectionView.messagesDataSource?.messageForItem(at: indexPath, in: messagesCollectionView) else {
+                print("Failed to identify message when audio cell receive tap gesture")
+                return
+        }
+        guard audioController.state != .stopped else {
+            // There is no audio sound playing - prepare to start playing for given audio message
+            audioController.playSound(for: message, in: cell)
+            return
+        }
+        if audioController.playingMessage?.messageId == message.messageId {
+            // tap occur in the current cell that is playing audio sound
+            if audioController.state == .playing {
+                audioController.pauseSound(for: message, in: cell)
+            } else {
+                audioController.resumeSound()
+            }
+        } else {
+            // tap occur in a difference cell that the one is currently playing sound. First stop currently playing and start the sound for given message
+            audioController.stopAnyOngoingPlaying()
+            audioController.playSound(for: message, in: cell)
+        }
+    }
+
+
+    func didStartAudio(in cell: AudioMessageCell) {
+        print("audio started")
+    }
+
+    func didStopAudio(in cell: AudioMessageCell) {
+        print("audio stopped")
+    }
+
+    func didPauseAudio(in cell: AudioMessageCell) {
+        print("audio paused")
+    }
+
+
     @objc func didTapBackground(in cell: MessageCollectionViewCell) {
         print("background of message tapped")
     }

+ 47 - 20
deltachat-ios/DC/Wrapper.swift

@@ -1,5 +1,6 @@
 import Foundation
 import UIKit
+import AVFoundation
 
 class DcContext {
     let contextPointer: OpaquePointer?
@@ -473,36 +474,62 @@ class DcMsg: MessageType {
 
         switch self.viewtype! {
         case .image:
-            if text.isEmpty {
-                return MessageKind.photo(Media(image: image))
-            }
-            let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)])
-            return MessageKind.photoText(Media(image: image, text: attributedString))
+            return createImageMessage(text: text)
         case .video:
-            if text.isEmpty {
-                return MessageKind.video(Media(url: fileURL))
-            }
-            let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)])
-            return MessageKind.videoText(Media(url: fileURL, text: attributedString))
+            return createVideoMessage(text: text)
+        case .voice, .audio:
+            return createAudioMessage(text: text)
         default:
             // TODO: custom views for audio, etc
             if let filename = self.filename {
-                let fileSize = self.filesize / 1024
-                let fileString = "\(self.filename ?? "???") (\(self.filesize / 1024) kB)"
-                let attributedFileString = NSMutableAttributedString(string: fileString,
-                                                                     attributes: [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 13.0)])
-                if !text.isEmpty {
-                    attributedFileString.append(NSAttributedString(string: "\n\n",
-                                                                   attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 7.0)]))
-                    attributedFileString.append(NSAttributedString(string: text,
-                                                                   attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)]))
+                if Utils.hasAudioSuffix(url: fileURL!) {
+                   return createAudioMessage(text: text)
                 }
-                return MessageKind.fileText(Media(text: attributedFileString))
+                return createFileMessage(text: text)
             }
             return MessageKind.text(text)
         }
     }()
 
+    internal func createVideoMessage(text: String) -> MessageKind {
+        if text.isEmpty {
+                       return MessageKind.video(Media(url: fileURL))
+                   }
+                   let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)])
+                   return MessageKind.videoText(Media(url: fileURL, text: attributedString))
+    }
+
+    internal func createImageMessage(text: String) -> MessageKind {
+        if text.isEmpty {
+            return MessageKind.photo(Media(image: image))
+        }
+        let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)])
+        return MessageKind.photoText(Media(image: image, text: attributedString))
+    }
+
+    internal func createAudioMessage(text: String) -> MessageKind {
+        let audioAsset = AVURLAsset(url: fileURL!)
+        let seconds = Float(CMTimeGetSeconds(audioAsset.duration))
+        if !text.isEmpty {
+            let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)])
+            return MessageKind.audio(Audio(url: audioAsset.url, duration: seconds, text: attributedString))
+        }
+        return MessageKind.audio(Audio(url: fileURL!, duration: seconds))
+    }
+
+    internal func createFileMessage(text: String) -> MessageKind {
+        let fileString = "\(self.filename ?? "???") (\(self.filesize / 1024) kB)"
+        let attributedFileString = NSMutableAttributedString(string: fileString,
+                                                             attributes: [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 13.0)])
+        if !text.isEmpty {
+            attributedFileString.append(NSAttributedString(string: "\n\n",
+                                                           attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 7.0)]))
+            attributedFileString.append(NSAttributedString(string: text,
+                                                           attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)]))
+        }
+        return MessageKind.fileText(Media(text: attributedFileString))
+    }
+
     var messageId: String {
         return "\(id)"
     }

+ 0 - 7
deltachat-ios/Extensions/UIImage+Extension.swift

@@ -95,13 +95,6 @@ extension UIImage {
         return UIImage(data: imageData!)
     }
 
-    public class func messageKitImageWith(type: ImageType) -> UIImage? {
-        let assetBundle = Bundle.messageKitAssetBundle()
-        let imagePath = assetBundle.path(forResource: type.rawValue, ofType: "png", inDirectory: "Images")
-        let image = UIImage(contentsOfFile: imagePath ?? "")
-        return image
-    }
-
 }
 
 public enum ImageType: String {

+ 5 - 0
deltachat-ios/Helper/Utils.swift

@@ -141,6 +141,11 @@ struct Utils {
         }
     }
 
+    static func hasAudioSuffix(url: URL) -> Bool {
+        ///TODO: add more file suffixes
+        return url.absoluteString.hasSuffix("wav")
+    }
+
     static func generateThumbnailFromVideo(url: URL) -> UIImage? {
         do {
             let asset = AVURLAsset(url: url)

+ 213 - 0
deltachat-ios/MessageKit/Controllers/BasicAudioController.swift

@@ -0,0 +1,213 @@
+/*
+ MIT License
+
+ Copyright (c) 2017-2019 MessageKit
+
+ 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.
+ */
+
+import UIKit
+import AVFoundation
+
+/// The `PlayerState` indicates the current audio controller state
+public enum PlayerState {
+
+    /// The audio controller is currently playing a sound
+    case playing
+
+    /// The audio controller is currently in pause state
+    case pause
+
+    /// The audio controller is not playing any sound and audioPlayer is nil
+    case stopped
+}
+
+/// The `BasicAudioController` update UI for current audio cell that is playing a sound
+/// and also creates and manage an `AVAudioPlayer` states, play, pause and stop.
+open class BasicAudioController: NSObject, AVAudioPlayerDelegate {
+
+    /// The `AVAudioPlayer` that is playing the sound
+    open var audioPlayer: AVAudioPlayer?
+
+    /// The `AudioMessageCell` that is currently playing sound
+    open weak var playingCell: AudioMessageCell?
+
+    /// The `MessageType` that is currently playing sound
+    open var playingMessage: MessageType?
+
+    /// Specify if current audio controller state: playing, in pause or none
+    open private(set) var state: PlayerState = .stopped
+
+    // The `MessagesCollectionView` where the playing cell exist
+    public weak var messageCollectionView: MessagesCollectionView?
+
+    /// The `Timer` that update playing progress
+    internal var progressTimer: Timer?
+
+    // MARK: - Init Methods
+
+    public init(messageCollectionView: MessagesCollectionView) {
+        self.messageCollectionView = messageCollectionView
+        super.init()
+    }
+
+    // MARK: - Methods
+
+    /// Used to configure the audio cell UI:
+    ///     1. play button selected state;
+    ///     2. progresssView progress;
+    ///     3. durationLabel text;
+    ///
+    /// - Parameters:
+    ///   - cell: The `AudioMessageCell` that needs to be configure.
+    ///   - message: The `MessageType` that configures the cell.
+    ///
+    /// - Note:
+    ///   This protocol method is called by MessageKit every time an audio cell needs to be configure
+    open func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
+        if playingMessage?.messageId == message.messageId, let collectionView = messageCollectionView, let player = audioPlayer {
+            playingCell = cell
+            cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime/player.duration)
+            cell.playButton.isSelected = (player.isPlaying == true) ? true : false
+            guard let displayDelegate = collectionView.messagesDisplayDelegate else {
+                fatalError("MessagesDisplayDelegate has not been set.")
+            }
+            cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.currentTime), for: cell, in: collectionView)
+        }
+    }
+
+    /// Used to start play audio sound
+    ///
+    /// - Parameters:
+    ///   - message: The `MessageType` that contain the audio item to be played.
+    ///   - audioCell: The `AudioMessageCell` that needs to be updated while audio is playing.
+    open func playSound(for message: MessageType, in audioCell: AudioMessageCell) {
+        switch message.kind {
+        case .audio(let item):
+            playingCell = audioCell
+            playingMessage = message
+            guard let player = try? AVAudioPlayer(contentsOf: item.url) else {
+                print("Failed to create audio player for URL: \(item.url)")
+                return
+            }
+            audioPlayer = player
+            audioPlayer?.prepareToPlay()
+            audioPlayer?.delegate = self
+            audioPlayer?.play()
+            state = .playing
+            audioCell.playButton.isSelected = true  // show pause button on audio cell
+            startProgressTimer()
+            audioCell.delegate?.didStartAudio(in: audioCell)
+        default:
+            print("BasicAudioPlayer failed play sound becasue given message kind is not Audio")
+        }
+    }
+
+    /// Used to pause the audio sound
+    ///
+    /// - Parameters:
+    ///   - message: The `MessageType` that contain the audio item to be pause.
+    ///   - audioCell: The `AudioMessageCell` that needs to be updated by the pause action.
+    open func pauseSound(for message: MessageType, in audioCell: AudioMessageCell) {
+        audioPlayer?.pause()
+        state = .pause
+        audioCell.playButton.isSelected = false // show play button on audio cell
+        progressTimer?.invalidate()
+        if let cell = playingCell {
+            cell.delegate?.didPauseAudio(in: cell)
+        }
+    }
+
+    /// Stops any ongoing audio playing if exists
+    open func stopAnyOngoingPlaying() {
+        guard let player = audioPlayer, let collectionView = messageCollectionView else { return } // If the audio player is nil then we don't need to go through the stopping logic
+        player.stop()
+        state = .stopped
+        if let cell = playingCell {
+            cell.progressView.progress = 0.0
+            cell.playButton.isSelected = false
+            guard let displayDelegate = collectionView.messagesDisplayDelegate else {
+                fatalError("MessagesDisplayDelegate has not been set.")
+            }
+            cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.duration), for: cell, in: collectionView)
+            cell.delegate?.didStopAudio(in: cell)
+        }
+        progressTimer?.invalidate()
+        progressTimer = nil
+        audioPlayer = nil
+        playingMessage = nil
+        playingCell = nil
+    }
+
+    /// Resume a currently pause audio sound
+    open func resumeSound() {
+        guard let player = audioPlayer, let cell = playingCell else {
+            stopAnyOngoingPlaying()
+            return
+        }
+        player.prepareToPlay()
+        player.play()
+        state = .playing
+        startProgressTimer()
+        cell.playButton.isSelected = true // show pause button on audio cell
+        cell.delegate?.didStartAudio(in: cell)
+    }
+
+    // MARK: - Fire Methods
+    @objc private func didFireProgressTimer(_ timer: Timer) {
+        guard let player = audioPlayer, let collectionView = messageCollectionView, let cell = playingCell else {
+            return
+        }
+        // check if can update playing cell
+        if let playingCellIndexPath = collectionView.indexPath(for: cell) {
+            // 1. get the current message that decorates the playing cell
+            // 2. check if current message is the same with playing message, if so then update the cell content
+            // Note: Those messages differ in the case of cell reuse
+            let currentMessage = collectionView.messagesDataSource?.messageForItem(at: playingCellIndexPath, in: collectionView)
+            if currentMessage != nil && currentMessage?.messageId == playingMessage?.messageId {
+                // messages are the same update cell content
+                cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime/player.duration)
+                guard let displayDelegate = collectionView.messagesDisplayDelegate else {
+                    fatalError("MessagesDisplayDelegate has not been set.")
+                }
+                cell.durationLabel.text = displayDelegate.audioProgressTextFormat(Float(player.currentTime), for: cell, in: collectionView)
+            } else {
+                // if the current message is not the same with playing message stop playing sound
+                stopAnyOngoingPlaying()
+            }
+        }
+    }
+
+    // MARK: - Private Methods
+    private func startProgressTimer() {
+        progressTimer?.invalidate()
+        progressTimer = nil
+        progressTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(BasicAudioController.didFireProgressTimer(_:)), userInfo: nil, repeats: true)
+    }
+
+    // MARK: - AVAudioPlayerDelegate
+    open func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        stopAnyOngoingPlaying()
+    }
+
+    open func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
+        stopAnyOngoingPlaying()
+    }
+
+}

+ 2 - 0
deltachat-ios/MessageKit/Protocols/AudioItem.swift

@@ -37,4 +37,6 @@ public protocol AudioItem {
     /// The size of the audio item.
     var size: CGSize { get }
 
+    /// Additional text
+    var text: NSAttributedString? { get }
 }

+ 3 - 3
deltachat-ios/MessageKit/Views/Cells/AudioMessageCell.swift

@@ -31,8 +31,8 @@ open class AudioMessageCell: MessageContentCell {
     /// The play button view to display on audio messages.
     public lazy var playButton: UIButton = {
         let playButton = UIButton(type: .custom)
-        let playImage = UIImage.messageKitImageWith(type: .play)
-        let pauseImage = UIImage.messageKitImageWith(type: .pause)
+        let playImage = UIImage(named: "play")
+        let pauseImage = UIImage(named: "pause")
         playButton.setImage(playImage?.withRenderingMode(.alwaysTemplate), for: .normal)
         playButton.setImage(pauseImage?.withRenderingMode(.alwaysTemplate), for: .selected)
         return playButton
@@ -57,7 +57,7 @@ open class AudioMessageCell: MessageContentCell {
 
     /// Responsible for setting up the constraints of the cell's subviews.
     open func setupConstraints() {
-        playButton.constraint(equalTo: CGSize(width: 25, height: 25))
+        playButton.constraint(equalTo: CGSize(width: 35, height: 35))
         playButton.addConstraints(left: messageContainerView.leftAnchor, centerY: messageContainerView.centerYAnchor, leftConstant: 5)
         durationLabel.addConstraints(right: messageContainerView.rightAnchor, centerY: messageContainerView.centerYAnchor, rightConstant: 15)
         progressView.addConstraints(left: playButton.rightAnchor,

+ 1 - 1
deltachat-ios/MessageKit/Views/Cells/ContactMessageCell.swift

@@ -56,7 +56,7 @@ open class ContactMessageCell: MessageContentCell {
     
     /// The disclouser image view
     public lazy var disclosureImageView: UIImageView = {
-        let disclouserImage = UIImage.messageKitImageWith(type: .disclouser)?.withRenderingMode(.alwaysTemplate)
+        let disclouserImage = UIImage(named: "disclouser")?.withRenderingMode(.alwaysTemplate)
         let disclouser = UIImageView(image: disclouserImage)
         return disclouser
     }()

+ 19 - 0
deltachat-ios/Model/Audio.swift

@@ -0,0 +1,19 @@
+import CoreLocation
+import Foundation
+import UIKit
+
+struct Audio: AudioItem {
+    var size: CGSize = CGSize(width: 250, height: 100)
+
+    var url: URL
+
+    var duration: Float
+
+    var text: NSAttributedString?
+
+    init(url: URL, duration: Float, text: NSAttributedString? = nil) {
+        self.url = url
+        self.duration = duration
+        self.text = text
+    }
+}