diff --git a/Signal/src/Calls/Group/GroupCallUpdateMessageHandler.swift b/Signal/src/Calls/Group/GroupCallUpdateMessageHandler.swift index e41d377071..6fc8a2cb32 100644 --- a/Signal/src/Calls/Group/GroupCallUpdateMessageHandler.swift +++ b/Signal/src/Calls/Group/GroupCallUpdateMessageHandler.swift @@ -4,6 +4,7 @@ import Foundation import SignalServiceKit +import SignalRingRTC class GroupCallUpdateMessageHandler: CallServiceObserver, CallObserver { diff --git a/Signal/src/Calls/SignalCall.swift b/Signal/src/Calls/SignalCall.swift index 90a09e9ad8..a8b094090c 100644 --- a/Signal/src/Calls/SignalCall.swift +++ b/Signal/src/Calls/SignalCall.swift @@ -286,3 +286,14 @@ extension SignalCall: IndividualCallDelegate { observers.elements.forEach { $0.individualCallRemoteVideoMuteDidChange(self, isVideoMuted: isVideoMuted) } } } + +extension GroupCall { + public var isFull: Bool { + guard let peekInfo = peekInfo, let maxDevices = peekInfo.maxDevices else { return false } + return peekInfo.deviceCount >= maxDevices + } + public var maxDevices: UInt32? { + guard let peekInfo = peekInfo, let maxDevices = peekInfo.maxDevices else { return nil } + return maxDevices + } +} diff --git a/Signal/src/Calls/UserInterface/CallControls.swift b/Signal/src/Calls/UserInterface/CallControls.swift index fb4126c7aa..7b58812db0 100644 --- a/Signal/src/Calls/UserInterface/CallControls.swift +++ b/Signal/src/Calls/UserInterface/CallControls.swift @@ -76,6 +76,14 @@ class CallControls: UIView { } button.contentEdgeInsets = UIEdgeInsets(top: 11, leading: 11, bottom: 11, trailing: 11) button.addSubview(joinButtonActivityIndicator) + button.setTitle( + NSLocalizedString( + "GROUP_CALL_IS_FULL", + comment: "Text explaining the group call is full" + ), + for: .disabled + ) + button.setTitleColor(.ows_whiteAlpha40, for: .disabled) joinButtonActivityIndicator.autoCenterInSuperview() return button }() @@ -221,12 +229,16 @@ class CallControls: UIView { let startCallText = NSLocalizedString("GROUP_CALL_START_BUTTON", comment: "Button to start a group call") let joinCallText = NSLocalizedString("GROUP_CALL_JOIN_BUTTON", comment: "Button to join an ongoing group call") - if call.groupCall.localDeviceState.joinState == .joining { + if call.groupCall.isFull { + joinButton.isEnabled = false + } else if call.groupCall.localDeviceState.joinState == .joining { + joinButton.isEnabled = true joinButton.isUserInteractionEnabled = false joinButtonActivityIndicator.startAnimating() joinButton.setTitle("", for: .normal) } else { + joinButton.isEnabled = true joinButton.isUserInteractionEnabled = true joinButtonActivityIndicator.stopAnimating() diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift b/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift index 7dcb199d50..a4226656cb 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift @@ -92,6 +92,41 @@ class GroupCallLocalMemberView: GroupCallMemberView { lazy var videoOffIndicatorWidthConstraint = videoOffIndicatorImage.autoSetDimension(.width, toSize: videoOffIndicatorWidth) + lazy var callFullLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.font = .ows_dynamicTypeSubheadline + label.textAlignment = .center + label.textColor = Theme.darkThemePrimaryColor + return label + }() + + lazy var callFullStack: UIStackView = { + let callFullStack = UIStackView() + callFullStack.axis = .vertical + callFullStack.spacing = 8 + + let imageView = UIImageView(image: #imageLiteral(resourceName: "sad-cat")) + imageView.contentMode = .scaleAspectFit + imageView.autoSetDimensions(to: CGSize(square: 200)) + callFullStack.addArrangedSubview(imageView) + + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString( + "GROUP_CALL_IS_FULL", + comment: "Text explaining the group call is full" + ) + titleLabel.font = UIFont.ows_dynamicTypeSubheadline.ows_semibold + titleLabel.textAlignment = .center + titleLabel.textColor = Theme.darkThemePrimaryColor + callFullStack.addArrangedSubview(titleLabel) + + callFullStack.addArrangedSubview(callFullLabel) + + return callFullStack + }() + override init() { super.init() @@ -114,6 +149,10 @@ class GroupCallLocalMemberView: GroupCallMemberView { insertSubview(videoView, belowSubview: muteIndicatorImage) videoView.frame = bounds + addSubview(callFullStack) + callFullStack.autoAlignAxis(.horizontal, toSameAxisOf: self, withOffset: -30) + callFullStack.autoPinWidthToSuperview(withMargin: 16) + layer.shadowOffset = .zero layer.shadowOpacity = 0.25 layer.shadowRadius = 4 @@ -130,7 +169,34 @@ class GroupCallLocalMemberView: GroupCallMemberView { videoView.isHidden = call.groupCall.isOutgoingVideoMuted videoView.captureSession = call.videoCaptureController.captureSession noVideoView.isHidden = !videoView.isHidden - videoOffLabel.isHidden = !videoView.isHidden || !isFullScreen + + if isFullScreen, + call.groupCall.isFull, + case .notJoined = call.groupCall.localDeviceState.joinState { + + let text: String + if let maxDevices = call.groupCall.maxDevices { + let formatString = NSLocalizedString( + "GROUP_CALL_HAS_MAX_DEVICES_FORMAT", + comment: "An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}." + ) + text = String(format: formatString, maxDevices) + } else { + text = NSLocalizedString( + "GROUP_CALL_HAS_MAX_DEVICES_UNKNOWN_COUNT", + comment: "An error displayed to the user when the group call ends because it has exceeded the max devices." + ) + } + + callFullLabel.text = text + callFullStack.isHidden = false + videoOffLabel.isHidden = true + videoOffIndicatorImage.isHidden = true + } else { + callFullStack.isHidden = true + videoOffLabel.isHidden = !videoView.isHidden || !isFullScreen + videoOffIndicatorImage.isHidden = !videoView.isHidden + } guard let localAddress = tsAccountManager.localAddress else { return owsFailDebug("missing local address") diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallNotificationView.swift b/Signal/src/Calls/UserInterface/Group/GroupCallNotificationView.swift index 26b799e3b5..48a86a2f08 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallNotificationView.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallNotificationView.swift @@ -142,6 +142,11 @@ extension GroupCallNotificationView: CallObserver { AssertIsOnMainThread() owsAssertDebug(call.isGroupCall) + hasJoined = false + activeMembers.removeAll() + membersPendingJoinNotification.removeAll() + membersPendingLeaveNotification.removeAll() + updateActiveMembers() } } diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift index 36ac463aa7..283430c6fb 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift @@ -471,14 +471,21 @@ extension GroupCallViewController: CallObserver { guard reason != .deviceExplicitlyDisconnected else { return } let title: String - switch reason { - case .hasMaxDevices: - let formatString = NSLocalizedString( - "GROUP_CALL_HAS_MAX_DEVICES_FORMAT", - comment: "An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}." - ) - title = String(format: formatString, groupCall.peekInfo?.maxDevices ?? 5) - default: + + if reason == .hasMaxDevices { + if let maxDevices = groupCall.maxDevices { + let formatString = NSLocalizedString( + "GROUP_CALL_HAS_MAX_DEVICES_FORMAT", + comment: "An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}." + ) + title = String(format: formatString, maxDevices) + } else { + title = NSLocalizedString( + "GROUP_CALL_HAS_MAX_DEVICES_UNKNOWN_COUNT", + comment: "An error displayed to the user when the group call ends because it has exceeded the max devices." + ) + } + } else { owsFailDebug("Group call ended with reason \(reason)") title = NSLocalizedString( "GROUP_CALL_UNEXPECTEDLY_ENDED", diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 73862c9003..e1d9bb13ec 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1528,6 +1528,12 @@ /* An error displayed to the user when the group call ends because it has exceeded the max devices. Embeds {{max device count}}. */ "GROUP_CALL_HAS_MAX_DEVICES_FORMAT" = "The maximum number of %ld participants has been reached for this call. Try again later."; +/* An error displayed to the user when the group call ends because it has exceeded the max devices. */ +"GROUP_CALL_HAS_MAX_DEVICES_UNKNOWN_COUNT" = "The maximum number of participants has been reached for this call. Try again later."; + +/* Text explaining the group call is full */ +"GROUP_CALL_IS_FULL" = "Call is Full"; + /* Button to join an ongoing group call */ "GROUP_CALL_JOIN_BUTTON" = "Join Call";