session-ios/Session/Calls/UserInterface/Group/GroupCallNotificationView.s...

267 lines
9.8 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalRingRTC
class GroupCallNotificationView: UIView {
private let call: SignalCall
private struct ActiveMember: Hashable {
let demuxId: UInt32
let uuid: UUID
var address: String { return "" }
}
private var activeMembers = Set<ActiveMember>()
private var membersPendingJoinNotification = Set<ActiveMember>()
private var membersPendingLeaveNotification = Set<ActiveMember>()
init(call: SignalCall) {
self.call = call
super.init(frame: .zero)
call.addObserverAndSyncState(observer: self)
isUserInteractionEnabled = false
}
deinit { call.removeObserver(self) }
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var hasJoined = false
private func updateActiveMembers() {
let newActiveMembers = Set(call.groupCall.remoteDeviceStates.values.map {
ActiveMember(demuxId: $0.demuxId, uuid: $0.userId)
})
if hasJoined {
let joinedMembers = newActiveMembers.subtracting(activeMembers)
let leftMembers = activeMembers.subtracting(newActiveMembers)
membersPendingJoinNotification.subtract(leftMembers)
membersPendingJoinNotification.formUnion(joinedMembers)
membersPendingLeaveNotification.subtract(joinedMembers)
membersPendingLeaveNotification.formUnion(leftMembers)
} else {
hasJoined = call.groupCall.localDeviceState.joinState == .joined
}
activeMembers = newActiveMembers
presentNextNotificationIfNecessary()
}
private var isPresentingNotification = false
private func presentNextNotificationIfNecessary() {
guard !isPresentingNotification else { return }
guard let bannerView: BannerView = {
if membersPendingJoinNotification.count > 0 {
callService.audioService.playJoinSound()
let addresses = membersPendingJoinNotification.map { $0.address }
membersPendingJoinNotification.removeAll()
return BannerView(addresses: addresses, action: .join)
} else if membersPendingLeaveNotification.count > 0 {
callService.audioService.playLeaveSound()
let addresses = membersPendingLeaveNotification.map { $0.address }
membersPendingLeaveNotification.removeAll()
return BannerView(addresses: addresses, action: .leave)
} else {
return nil
}
}() else { return }
isPresentingNotification = true
addSubview(bannerView)
bannerView.autoHCenterInSuperview()
// Prefer to be full width, but don't exceed the maximum width
bannerView.autoSetDimension(.width, toSize: 512, relation: .lessThanOrEqual)
bannerView.autoMatch(
.width,
to: .width,
of: self,
withOffset: -(layoutMargins.left + layoutMargins.right),
relation: .lessThanOrEqual
)
NSLayoutConstraint.autoSetPriority(.defaultHigh) {
bannerView.autoPinWidthToSuperviewMargins()
}
let onScreenConstraint = bannerView.autoPinEdge(toSuperviewMargin: .top)
onScreenConstraint.isActive = false
let offScreenConstraint = bannerView.autoPinEdge(.bottom, to: .top, of: self)
layoutIfNeeded()
firstly(on: .main) {
UIView.animate(.promise, duration: 0.35) {
offScreenConstraint.isActive = false
onScreenConstraint.isActive = true
self.layoutIfNeeded()
}
}.then(on: .main) { _ in
UIView.animate(.promise, duration: 0.35, delay: 2, options: .curveEaseInOut) {
onScreenConstraint.isActive = false
offScreenConstraint.isActive = true
self.layoutIfNeeded()
}
}.done(on: .main) { _ in
bannerView.removeFromSuperview()
self.isPresentingNotification = false
self.presentNextNotificationIfNecessary()
}
}
}
extension GroupCallNotificationView: CallObserver {
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {
AssertIsOnMainThread()
owsAssertDebug(call.isGroupCall)
updateActiveMembers()
}
func groupCallPeekChanged(_ call: SignalCall) {
AssertIsOnMainThread()
owsAssertDebug(call.isGroupCall)
updateActiveMembers()
}
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) {
AssertIsOnMainThread()
owsAssertDebug(call.isGroupCall)
hasJoined = false
activeMembers.removeAll()
membersPendingJoinNotification.removeAll()
membersPendingLeaveNotification.removeAll()
updateActiveMembers()
}
}
private class BannerView: UIView {
enum Action: Equatable { case join, leave }
init(addresses: [String], action: Action) {
super.init(frame: .zero)
owsAssertDebug(!addresses.isEmpty)
autoSetDimension(.height, toSize: 64, relation: .greaterThanOrEqual)
layer.cornerRadius = 8
clipsToBounds = true
if UIAccessibility.isReduceTransparencyEnabled {
backgroundColor = .black.withAlphaComponent(0.8)
} else {
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
addSubview(blurEffectView)
blurEffectView.autoPinEdgesToSuperviewEdges()
backgroundColor = .black.withAlphaComponent(0.4)
}
let displayNames = addresses.map { publicKey in
Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
}.sorted { $0 < $1 }
let actionText: String
if displayNames.count > 2 {
let formatText = action == .join
? NSLocalizedString(
"GROUP_CALL_NOTIFICATION_MANY_JOINED_FORMAT",
comment: "Copy explaining that many new users have joined the group call. Embeds {first member name}, {second member name}, {number of additional members}"
)
: NSLocalizedString(
"GROUP_CALL_NOTIFICATION_MANY_LEFT_FORMAT",
comment: "Copy explaining that many users have left the group call. Embeds {first member name}, {second member name}, {number of additional members}"
)
actionText = String(format: formatText, displayNames[0], displayNames[1], displayNames.count - 2)
} else if displayNames.count > 1 {
let formatText = action == .join
? NSLocalizedString(
"GROUP_CALL_NOTIFICATION_TWO_JOINED_FORMAT",
comment: "Copy explaining that two users have joined the group call. Embeds {first member name}, {second member name}"
)
: NSLocalizedString(
"GROUP_CALL_NOTIFICATION_TWO_LEFT_FORMAT",
comment: "Copy explaining that two users have left the group call. Embeds {first member name}, {second member name}"
)
actionText = String(format: formatText, displayNames[0], displayNames[1])
} else {
let formatText = action == .join
? NSLocalizedString(
"GROUP_CALL_NOTIFICATION_ONE_JOINED_FORMAT",
comment: "Copy explaining that a user has joined the group call. Embeds {member name}"
)
: NSLocalizedString(
"GROUP_CALL_NOTIFICATION_ONE_LEFT_FORMAT",
comment: "Copy explaining that a user has left the group call. Embeds {member name}"
)
actionText = String(format: formatText, displayNames[0])
}
let hStack = UIStackView()
hStack.spacing = 12
hStack.axis = .horizontal
hStack.isLayoutMarginsRelativeArrangement = true
hStack.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
addSubview(hStack)
hStack.autoPinEdgesToSuperviewEdges()
if addresses.count == 1, let address = addresses.first {
let avatarContainer = UIView()
hStack.addArrangedSubview(avatarContainer)
avatarContainer.autoSetDimension(.width, toSize: 40)
let avatarView = UIImageView()
avatarView.layer.cornerRadius = 20
avatarView.clipsToBounds = true
avatarContainer.addSubview(avatarView)
avatarView.autoPinWidthToSuperview()
avatarView.autoVCenterInSuperview()
avatarView.autoMatch(.height, to: .width, of: avatarView)
// TODO: Implement
/*
if address.isLocalAddress,
let avatarImage = profileManager.localProfileAvatarImage() {
avatarView.image = avatarImage
} else {
let avatar = Self.avatarBuilder.avatarImageWithSneakyTransaction(forAddress: address,
diameterPoints: 40,
localUserDisplayMode: .asUser)
avatarView.image = avatar
}
*/
}
let label = UILabel()
hStack.addArrangedSubview(label)
label.setCompressionResistanceHorizontalHigh()
label.numberOfLines = 0
label.font = .boldSystemFont(ofSize: Values.mediumFontSize)
label.textColor = .ows_white
label.text = actionText
hStack.addArrangedSubview(.hStretchingSpacer())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}