                 return
             }
             
-            //  there is no reject event, we can just say this call has ended
-            statusTextView.text = VectorL10n.eventFormatterCallHasEnded
+            //  check events include an answer event
+            if !events.contains(where: { $0.eventType == .callAnswer }) {
+                configureForUnansweredCall()
+                return
+            }
+            
+            //  check events include a hangup event
+            if let hangupEvent = events.first(where: { $0.eventType == .callHangup }) {
+                configureForHangupCall(withEvent: hangupEvent)
+                return
+            }
+            
+            //  there is no reject or hangup event, we can just say this call has ended
+            viewState = .ended
+            statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString)
             return
         }
         
         configure(withCall: call)
     }
     
-    override func prepareForReuse() {
-        statusTextView.isSelectable = false
-        statusTextView.text = nil
-        statusTextView.attributedText = nil
-        
-        super.prepareForReuse()
+    private func callDuration(from events: [MXEvent]) -> TimeInterval {
+        guard let startDate = events.first(where: { $0.eventType == .callAnswer })?.originServerTs else {
+            //  never started
+            return 0
+        }
+        guard let endDate = events.first(where: { $0.eventType == .callHangup })?.originServerTs
+                ?? events.first(where: { $0.eventType == .callReject })?.originServerTs else {
+            //  not ended yet, compute the diff from now
+            return (NSTimeIntervalSince1970 - TimeInterval(startDate))/MSEC_PER_SEC
+        }
+        
+        //  ended, compute the diff between two dates
+        return TimeInterval(endDate - startDate)/MSEC_PER_SEC
+    }
+    
+    private func readableCallDuration(from events: [MXEvent]) -> String {
+        let duration = callDuration(from: events)
+        
+        if duration <= 0 {
+            return ""
+        }
+        
+        return RoomDirectCallStatusBubbleCell.callDurationFormatter.string(from: duration) ?? ""
     }
     
 }
-
-//  MARK: - UITextViewDelegate
-
-extension RoomDirectCallStatusBubbleCell {
-    
-    override func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
-        if URL == Constants.statusCallBackURL && interaction == .invokeDefaultAction {
-            let userInfo: [AnyHashable: Any]?
-            
-            guard let bubbleCellData = bubbleData as? RoomBubbleCellData else {
-                return false
-            }
-            let events = bubbleCellData.allLinkedEvents()
-            if let callInviteEvent = events.first(where: { $0.eventType == .callInvite }) {
-                userInfo = [kMXKRoomBubbleCellEventKey: callInviteEvent]
-            } else {
-                userInfo = nil
-            }
-            
-            self.delegate?.cell(self, didRecognizeAction: kMXKRoomBubbleCellCallBackButtonPressed, userInfo: userInfo)
-            return true
-        }
-        return false
-    }
-    
-}

         .connecting,
         .onHold,
         .remotelyOnHold:
            statusTextView.text = VectorL10n.eventFormatterCallYouCurrentlyIn
        case .ringing:
            if call.isIncoming {
                //  should not be here
                statusTextView.text = nil
            } else {
                statusTextView.text = VectorL10n.eventFormatterCallYouCurrentlyIn
            }
        case .ended:
            switch call.endReason {


import UIKit

/// The number of milliseconds in one second.
private let MSEC_PER_SEC: TimeInterval = 1000

class RoomDirectCallStatusBubbleCell: RoomBaseCallBubbleCell {
    
    private var callDurationString: String = ""
    private var isVideoCall: Bool = false
    private var isIncoming: Bool = false
    private var callInviteEvent: MXEvent?
    private var viewState: ViewState = .unknown {
        didSet {
            updateBottomContentView()
        }
    }
    
    private enum ViewState {
        case unknown
        case ringing
        case active
        case declined
        case missed
        case ended
        case failed
    }
    
    private static var callDurationFormatter: DateComponentsFormatter {
        let formatter = DateComponentsFormatter()
        formatter.zeroFormattingBehavior = .dropAll
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        return formatter
    }
    
    override func update(theme: Theme) {
        super.update(theme: theme)
        if let themable = bottomContentView as? Themable {
            themable.update(theme: theme)
        }
    }
    
    private func updateBottomContentView() {
        bottomContentView = bottomView(for: viewState)
    }
    
    private var callTypeIcon: UIImage {
        if isVideoCall {
            return Asset.Images.callVideoIcon.image
        } else {
            return Asset.Images.voiceCallHangonIcon.image
        }
    }
    
    private var actionUserInfo: [AnyHashable: Any]? {
        if let event = callInviteEvent {
            return [kMXKRoomBubbleCellEventKey: event]
        }
        return nil
    }
    
    private func bottomView(for state: ViewState) -> UIView? {
        switch state {
        case .unknown:
            return nil
        case .ringing:
            let view = HorizontalButtonsContainerView.loadFromNib()
            
            view.firstButton.style = .negative
            view.firstButton.setTitle(VectorL10n.eventFormatterCallDecline, for: .normal)
            view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(declineCallAction(_:)), for: .touchUpInside)
            
            view.secondButton.style = .positive
            view.secondButton.setTitle(VectorL10n.eventFormatterCallAnswer, for: .normal)
            view.secondButton.setImage(callTypeIcon, for: .normal)
            view.secondButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.secondButton.addTarget(self, action: #selector(answerCallAction(_:)), for: .touchUpInside)
            
            return view
        case .active:
            return nil
        case .declined:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .positive
            view.firstButton.setTitle(VectorL10n.eventFormatterCallBack, for: .normal)
            view.firstButton.setImage(callTypeIcon, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside)
            
            return view
        case .missed:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .positive
            view.firstButton.setTitle(VectorL10n.eventFormatterCallBack, for: .normal)
            view.firstButton.setImage(callTypeIcon, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside)
            
            return view
        case .ended:
            return nil
        case .failed:
            let view = HorizontalButtonsContainerView.loadFromNib()
            view.secondButton.isHidden = true
            
            view.firstButton.style = .positive
            view.firstButton.setTitle(VectorL10n.eventFormatterCallRetry, for: .normal)
            view.firstButton.setImage(callTypeIcon, for: .normal)
            view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside)
            view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside)
            
            return view
        }
    }
    
    private func configure(withCall call: MXCall) {
