//
// C o p y r i g h t ( c ) 2 0 2 1 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import SignalRingRTC
@objc
protocol CallHeaderDelegate : AnyObject {
func didTapBackButton ( )
func didTapMembersButton ( )
}
class CallHeader : UIView {
// MARK: - V i e w s
private lazy var dateFormatter : DateFormatter = {
let dateFormatter = DateFormatter ( )
dateFormatter . dateFormat = " HH:mm:ss "
dateFormatter . timeZone = TimeZone ( identifier : " UTC " ) !
dateFormatter . locale = Locale ( identifier : " en_US " )
return dateFormatter
} ( )
private var callDurationTimer : Timer ?
private let callTitleLabel = MarqueeLabel ( )
private let callStatusLabel = UILabel ( )
private let groupMembersButton = GroupMembersButton ( )
private let call : SignalCall
private weak var delegate : CallHeaderDelegate !
init ( call : SignalCall , delegate : CallHeaderDelegate ) {
self . call = call
self . delegate = delegate
super . init ( frame : . zero )
call . addObserverAndSyncState ( observer : self )
let gradientLayer = CAGradientLayer ( )
gradientLayer . colors = [
UIColor . black . withAlphaComponent ( 0.6 ) . cgColor ,
UIColor . black . withAlphaComponent ( 0 ) . cgColor
]
let gradientView = OWSLayerView ( frame : . zero ) { view in
gradientLayer . frame = view . bounds
}
gradientView . layer . addSublayer ( gradientLayer )
addSubview ( gradientView )
gradientView . autoPinEdgesToSuperviewEdges ( )
let hStack = UIStackView ( )
hStack . axis = . horizontal
hStack . spacing = 13
hStack . layoutMargins = UIEdgeInsets ( top : 0 , left : 8 , bottom : 0 , right : 8 )
hStack . isLayoutMarginsRelativeArrangement = true
addSubview ( hStack )
hStack . autoPinWidthToSuperview ( )
hStack . autoPinEdge ( toSuperviewMargin : . top )
hStack . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 46 )
// B a c k b u t t o n
let backButton = UIButton ( )
let backButtonImage = CurrentAppContext ( ) . isRTL ? # imageLiteral ( resourceName : " NavBarBackRTL " ) : # imageLiteral ( resourceName : " NavBarBack " )
backButton . setTemplateImage ( backButtonImage , tintColor : . white )
backButton . autoSetDimensions ( to : CGSize ( square : 40 ) )
backButton . imageEdgeInsets = UIEdgeInsets ( top : - 12 , leading : - 18 , bottom : 0 , trailing : 0 )
backButton . addTarget ( delegate , action : #selector ( CallHeaderDelegate . didTapBackButton ) , for : . touchUpInside )
addShadow ( to : backButton )
hStack . addArrangedSubview ( backButton )
// v S t a c k
let vStack = UIStackView ( )
vStack . axis = . vertical
vStack . spacing = 4
hStack . addArrangedSubview ( vStack )
// N a m e L a b e l
callTitleLabel . type = . continuous
// T h i s f e e l s p r e t t y s l o w w h e n y o u ' r e i n i t i a l l y w a i t i n g f o r i t , b u t w h e n y o u ' r e o v e r l a y i n g v i d e o c a l l s , a n y t h i n g f a s t e r i s d i s t r a c t i n g .
callTitleLabel . speed = . duration ( 30.0 )
callTitleLabel . animationCurve = . linear
callTitleLabel . fadeLength = 10.0
callTitleLabel . animationDelay = 5
// A d d t r a i l i n g s p a c e a f t e r t h e n a m e s c r o l l s b e f o r e i t w r a p s a r o u n d a n d s c r o l l s b a c k i n .
callTitleLabel . trailingBuffer = ScaleFromIPhone5 ( 80.0 )
callTitleLabel . font = . boldSystemFont ( ofSize : Values . mediumFontSize )
callTitleLabel . textAlignment = . center
callTitleLabel . textColor = UIColor . white
addShadow ( to : callTitleLabel )
vStack . addArrangedSubview ( callTitleLabel )
// S t a t u s l a b e l
callStatusLabel . font = UIFont . ows_dynamicTypeFootnoteClamped
callStatusLabel . textAlignment = . center
callStatusLabel . textColor = UIColor . white
addShadow ( to : callStatusLabel )
vStack . addArrangedSubview ( callStatusLabel )
// G r o u p m e m b e r s b u t t o n
groupMembersButton . addTarget (
delegate ,
action : #selector ( CallHeaderDelegate . didTapMembersButton ) ,
for : . touchUpInside
)
addShadow ( to : groupMembersButton )
hStack . addArrangedSubview ( groupMembersButton )
updateCallTitleLabel ( )
updateCallStatusLabel ( )
updateGroupMembersButton ( )
}
deinit { call . removeObserver ( self ) }
private func addShadow ( to view : UIView ) {
view . layer . shadowOffset = . zero
view . layer . shadowOpacity = 0.25
view . layer . shadowRadius = 4
}
private func updateCallStatusLabel ( ) {
let callStatusText : String
switch call . groupCall . localDeviceState . joinState {
case . notJoined , . joining :
callStatusText = " "
case . joined :
let callDuration = call . connectionDuration ( )
let callDurationDate = Date ( timeIntervalSinceReferenceDate : callDuration )
var formattedDate = dateFormatter . string ( from : callDurationDate )
if formattedDate . hasPrefix ( " 00: " ) {
// D o n ' t s h o w t h e " h o u r s " p o r t i o n o f t h e d a t e f o r m a t u n l e s s t h e
// c a l l d u r a t i o n i s a t l e a s t 1 h o u r .
formattedDate = String ( formattedDate [ formattedDate . index ( formattedDate . startIndex , offsetBy : 3 ) . . . ] )
} else {
// I f s h o w i n g t h e " h o u r s " p o r t i o n o f t h e d a t e f o r m a t , s t r i p a n y l e a d i n g
// z e r o e s .
if formattedDate . hasPrefix ( " 0 " ) {
formattedDate = String ( formattedDate [ formattedDate . index ( formattedDate . startIndex , offsetBy : 1 ) . . . ] )
}
}
callStatusText = String ( format : CallStrings . callStatusFormat , formattedDate )
}
callStatusLabel . text = callStatusText
callStatusLabel . isHidden = call . groupCall . localDeviceState . joinState != . joined || call . groupCall . remoteDeviceStates . count > 1
}
func updateCallTitleLabel ( ) {
let callTitleText : String
if call . groupCall . localDeviceState . connectionState = = . reconnecting {
callTitleText = NSLocalizedString (
" GROUP_CALL_RECONNECTING " ,
comment : " Text indicating that the user has lost their connection to the call and we are reconnecting. "
)
} else {
var isFirstMemberPresenting = false
let memberNames : [ String ] = databaseStorage . read { transaction in
if self . call . groupCall . localDeviceState . joinState = = . joined {
let sortedDeviceStates = self . call . groupCall . remoteDeviceStates . sortedByAddedTime
isFirstMemberPresenting = sortedDeviceStates . first ? . presenting = = true
return sortedDeviceStates . map { self . contactsManager . displayName ( for : $0 . address , transaction : transaction ) }
} else {
return self . call . groupCall . peekInfo ? . joinedMembers
. map { self . contactsManager . displayName ( for : SignalServiceAddress ( uuid : $0 ) , transaction : transaction ) } ? ? [ ]
}
}
switch call . groupCall . localDeviceState . joinState {
case . joined :
switch memberNames . count {
case 0 :
callTitleText = NSLocalizedString (
" GROUP_CALL_NO_ONE_HERE " ,
comment : " Text explaining that you are the only person currently in the group call "
)
case 1 :
if isFirstMemberPresenting {
let formatString = NSLocalizedString (
" GROUP_CALL_PRESENTING_FORMAT " ,
comment : " Text explaining that a member is presenting. Embeds {member name} "
)
callTitleText = String ( format : formatString , memberNames [ 0 ] )
} else {
callTitleText = memberNames [ 0 ]
}
default :
if isFirstMemberPresenting {
let formatString = NSLocalizedString (
" GROUP_CALL_PRESENTING_FORMAT " ,
comment : " Text explaining that a member is presenting. Embeds {member name} "
)
callTitleText = String ( format : formatString , memberNames [ 0 ] )
} else {
callTitleText = " "
}
}
default :
switch memberNames . count {
case 0 :
callTitleText = " "
case 1 :
let formatString = NSLocalizedString (
" GROUP_CALL_ONE_PERSON_HERE_FORMAT " ,
comment : " Text explaining that there is one person in the group call. Embeds {member name} "
)
callTitleText = String ( format : formatString , memberNames [ 0 ] )
case 2 :
let formatString = NSLocalizedString (
" GROUP_CALL_TWO_PEOPLE_HERE_FORMAT " ,
comment : " Text explaining that there are two people in the group call. Embeds {{ %1$@ participant1, %2$@ participant2 }} "
)
callTitleText = String ( format : formatString , memberNames [ 0 ] , memberNames [ 1 ] )
case 3 :
let formatString = NSLocalizedString (
" GROUP_CALL_THREE_PEOPLE_HERE_FORMAT " ,
comment : " Text explaining that there are three people in the group call. Embeds {{ %1$@ participant1, %2$@ participant2 }} "
)
callTitleText = String ( format : formatString , memberNames [ 0 ] , memberNames [ 1 ] )
default :
let formatString = NSLocalizedString (
" GROUP_CALL_MANY_PEOPLE_HERE_FORMAT " ,
comment : " Text explaining that there are more than three people in the group call. Embeds {{ %1$@ participant1, %2$@ participant2, %3$@ participantCount-2 }} "
)
callTitleText = String ( format : formatString , memberNames [ 0 ] , memberNames [ 1 ] , OWSFormat . formatInt ( Int32 ( memberNames . count ) - 2 ) )
}
}
}
callTitleLabel . text = callTitleText
callTitleLabel . isHidden = callTitleText . isEmpty
}
func updateGroupMembersButton ( ) {
let isJoined = call . groupCall . localDeviceState . joinState = = . joined
let remoteMemberCount = isJoined ? call . groupCall . remoteDeviceStates . count : Int ( call . groupCall . peekInfo ? . deviceCount ? ? 0 )
groupMembersButton . updateMemberCount ( remoteMemberCount + ( isJoined ? 1 : 0 ) )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
}
extension CallHeader : CallObserver {
func groupCallLocalDeviceStateChanged ( _ call : SignalCall ) {
owsAssertDebug ( call . isGroupCall )
if call . groupCall . localDeviceState . joinState = = . joined {
if callDurationTimer = = nil {
let kDurationUpdateFrequencySeconds = 1 / 20.0
callDurationTimer = WeakTimer . scheduledTimer (
timeInterval : TimeInterval ( kDurationUpdateFrequencySeconds ) ,
target : self ,
userInfo : nil ,
repeats : true
) { [ weak self ] _ in
self ? . updateCallStatusLabel ( )
}
}
} else {
callDurationTimer ? . invalidate ( )
callDurationTimer = nil
}
updateCallTitleLabel ( )
updateCallStatusLabel ( )
updateGroupMembersButton ( )
}
func groupCallPeekChanged ( _ call : SignalCall ) {
updateCallTitleLabel ( )
updateGroupMembersButton ( )
}
func groupCallRemoteDeviceStatesChanged ( _ call : SignalCall ) {
updateCallTitleLabel ( )
updateGroupMembersButton ( )
}
func groupCallEnded ( _ call : SignalCall , reason : GroupCallEndReason ) {
callDurationTimer ? . invalidate ( )
callDurationTimer = nil
updateCallTitleLabel ( )
updateCallStatusLabel ( )
updateGroupMembersButton ( )
}
}
private class GroupMembersButton : UIButton {
private let iconImageView = UIImageView ( )
private let countLabel = UILabel ( )
override init ( frame : CGRect ) {
super . init ( frame : frame )
autoSetDimension ( . height , toSize : 40 )
iconImageView . contentMode = . scaleAspectFit
iconImageView . setTemplateImage ( # imageLiteral ( resourceName : " group-solid-24 " ) , tintColor : . white )
addSubview ( iconImageView )
iconImageView . autoPinEdge ( toSuperviewEdge : . leading )
iconImageView . autoSetDimensions ( to : CGSize ( width : 22 , height : 22 ) )
iconImageView . autoPinEdge ( toSuperviewEdge : . top , withInset : 2 )
countLabel . font = . systemFont ( ofSize : Values . mediumFontSize )
countLabel . textColor = . ows_white
addSubview ( countLabel )
countLabel . autoPinEdge ( . leading , to : . trailing , of : iconImageView , withOffset : 5 )
countLabel . autoPinEdge ( toSuperviewEdge : . trailing , withInset : 5 )
countLabel . autoAlignAxis ( . horizontal , toSameAxisOf : iconImageView )
countLabel . setContentHuggingHorizontalHigh ( )
countLabel . setCompressionResistanceHorizontalHigh ( )
}
func updateMemberCount ( _ count : Int ) {
countLabel . text = String ( OWSFormat . formatInt ( count ) )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override var isHighlighted : Bool {
didSet {
alpha = isHighlighted ? 0.5 : 1
}
}
}