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.
263 lines
9.7 KiB
Swift
263 lines
9.7 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)
|
|
|
|
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")
|
|
}
|
|
}
|