mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
13 KiB
Swift
344 lines
13 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalRingRTC
|
|
|
|
@objc
|
|
protocol CallHeaderDelegate: AnyObject {
|
|
func didTapBackButton()
|
|
func didTapMembersButton()
|
|
}
|
|
|
|
class CallHeader: UIView {
|
|
// MARK: - Views
|
|
|
|
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)
|
|
|
|
// Back button
|
|
|
|
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)
|
|
|
|
// vStack
|
|
|
|
let vStack = UIStackView()
|
|
vStack.axis = .vertical
|
|
vStack.spacing = 4
|
|
|
|
hStack.addArrangedSubview(vStack)
|
|
|
|
// Name Label
|
|
|
|
callTitleLabel.type = .continuous
|
|
// This feels pretty slow when you're initially waiting for it, but when you're overlaying video calls, anything faster is distracting.
|
|
callTitleLabel.speed = .duration(30.0)
|
|
callTitleLabel.animationCurve = .linear
|
|
callTitleLabel.fadeLength = 10.0
|
|
callTitleLabel.animationDelay = 5
|
|
// Add trailing space after the name scrolls before it wraps around and scrolls back in.
|
|
callTitleLabel.trailingBuffer = ScaleFromIPhone5(80.0)
|
|
|
|
callTitleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
|
callTitleLabel.textAlignment = .center
|
|
callTitleLabel.textColor = UIColor.white
|
|
addShadow(to: callTitleLabel)
|
|
|
|
vStack.addArrangedSubview(callTitleLabel)
|
|
|
|
// Status label
|
|
|
|
callStatusLabel.font = UIFont.ows_dynamicTypeFootnoteClamped
|
|
callStatusLabel.textAlignment = .center
|
|
callStatusLabel.textColor = UIColor.white
|
|
addShadow(to: callStatusLabel)
|
|
|
|
vStack.addArrangedSubview(callStatusLabel)
|
|
|
|
// Group members button
|
|
|
|
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:") {
|
|
// Don't show the "hours" portion of the date format unless the
|
|
// call duration is at least 1 hour.
|
|
formattedDate = String(formattedDate[formattedDate.index(formattedDate.startIndex, offsetBy: 3)...])
|
|
} else {
|
|
// If showing the "hours" portion of the date format, strip any leading
|
|
// zeroes.
|
|
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
|
|
}
|
|
}
|
|
}
|