diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallErrorView.swift b/Signal/src/Calls/UserInterface/Group/GroupCallErrorView.swift new file mode 100644 index 0000000000..6265b6097b --- /dev/null +++ b/Signal/src/Calls/UserInterface/Group/GroupCallErrorView.swift @@ -0,0 +1,135 @@ +// +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// + +import Foundation + +class GroupCallErrorView: UIView { + + // MARK: - Interface + + var forceCompactAppearance: Bool = false { + didSet { configure() } + } + + var iconImage: UIImage? { + didSet { + if let iconImage = iconImage { + iconView.setTemplateImage(iconImage, tintColor: .ows_white) + miniButton.setTemplateImage(iconImage, tintColor: .ows_white) + } else { + iconView.image = nil + miniButton.setImage(nil, for: .normal) + } + } + } + + var labelText: String? { + didSet { + label.text = labelText + } + } + + var userTapAction: ((GroupCallErrorView) -> Void)? + + func resetConfiguration() { + iconImage = nil + labelText = nil + userTapAction = nil + } + + // MARK: - Views + + private let iconView: UIImageView = { + let view = UIImageView() + view.setTemplateImageName("error-solid-16", tintColor: .ows_white) + return view + }() + + private let label: UILabel = { + let label = UILabel() + label.font = UIFont.ows_dynamicTypeSubheadline + label.textAlignment = .center + label.textColor = .ows_white + label.numberOfLines = 0 + return label + }() + + private let button: UIButton = { + let buttonLabel = NSLocalizedString( + "GROUP_CALL_ERROR_DETAILS", + comment: "A button to receive more info about not seeing a participant in group call grid") + + let button = UIButton() + button.backgroundColor = UIColor.darkGray + button.layer.cornerRadius = 16 + button.titleLabel?.textAlignment = .center + button.contentEdgeInsets = UIEdgeInsets(top: 3, leading: 12, bottom: 3, trailing: 12) + button.clipsToBounds = true + button.setTitle(buttonLabel, for: .normal) + button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) + return button + }() + + private let miniButton: UIButton = { + let button = UIButton() + button.contentVerticalAlignment = .fill + button.contentHorizontalAlignment = .fill + button.setTemplateImageName("error-solid-16", tintColor: .ows_white) + button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + let stackView = UIStackView(arrangedSubviews: [ + iconView, + label, + button, + ]) + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .fill + + stackView.setCustomSpacing(12, after: iconView) + stackView.setCustomSpacing(16, after: label) + + addSubview(stackView) + stackView.autoPinWidthToSuperviewMargins() + stackView.autoVCenterInSuperview() + stackView.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual) + stackView.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual) + + iconView.autoSetDimensions(to: CGSize(width: 24, height: 24)) + + addSubview(miniButton) + miniButton.autoSetDimensions(to: CGSize(width: 24, height: 24)) + miniButton.autoCenterInSuperview() + configure() + } + + override var bounds: CGRect { + didSet { configure() } + } + + override var frame: CGRect { + didSet { configure() } + } + + private func configure() { + let isCompact = (bounds.width < 100) || (bounds.height < 100) || forceCompactAppearance + iconView.isHidden = isCompact + label.isHidden = isCompact + button.isHidden = isCompact + miniButton.isHidden = !isCompact + } + + @objc + private func didTapButton() { + userTapAction?(self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift b/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift index 4651e50483..044f9f9fe8 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift @@ -5,7 +5,12 @@ import Foundation import SignalRingRTC +protocol GroupCallMemberViewDelegate: class { + func memberView(_: GroupCallMemberView, userRequestedInfoAboutError: GroupCallMemberView.ErrorState) +} + class GroupCallMemberView: UIView { + weak var delegate: GroupCallMemberViewDelegate? let noVideoView = UIView() let backgroundAvatarView = UIImageView() @@ -66,6 +71,11 @@ class GroupCallMemberView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + enum ErrorState { + case blocked(SignalServiceAddress) + case noMediaKeys(SignalServiceAddress) + } } class GroupCallLocalMemberView: GroupCallMemberView { @@ -396,9 +406,14 @@ class GroupCallRemoteMemberView: GroupCallMemberView { errorView.iconImage = image errorView.labelText = label - errorView.userTapAction = { _ in - // TODO - Logger.info("Present alert controller for \(displayName)") + errorView.userTapAction = { [weak self] _ in + guard let self = self else { return } + + if isBlocked { + self.delegate?.memberView(self, userRequestedInfoAboutError: .blocked(address)) + } else { + self.delegate?.memberView(self, userRequestedInfoAboutError: .noMediaKeys(address)) + } } } } diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallVideoGrid.swift b/Signal/src/Calls/UserInterface/Group/GroupCallVideoGrid.swift index 19078eefb4..ffb3905eb6 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallVideoGrid.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallVideoGrid.swift @@ -6,6 +6,7 @@ import Foundation import SignalRingRTC class GroupCallVideoGrid: UICollectionView { + weak var memberViewDelegate: GroupCallMemberViewDelegate? let layout: GroupCallVideoGridLayout let call: SignalCall init(call: SignalCall) { @@ -65,8 +66,8 @@ extension GroupCallVideoGrid: UICollectionViewDataSource { return cell } + cell.setMemberViewDelegate(memberViewDelegate) cell.configure(call: call, device: remoteDevice) - return cell } } @@ -149,6 +150,10 @@ class GroupCallVideoGridCell: UICollectionViewCell { func configureRemoteVideo(device: RemoteDeviceState) { memberView.configureRemoteVideo(device: device) } + + func setMemberViewDelegate(_ delegate: GroupCallMemberViewDelegate?) { + memberView.delegate = delegate + } } extension Sequence where Element: RemoteDeviceState { diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallVideoOverflow.swift b/Signal/src/Calls/UserInterface/Group/GroupCallVideoOverflow.swift index 19acbfe378..df0ded761f 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallVideoOverflow.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallVideoOverflow.swift @@ -11,6 +11,7 @@ protocol GroupCallVideoOverflowDelegate: class { } class GroupCallVideoOverflow: UICollectionView { + weak var memberViewDelegate: GroupCallMemberViewDelegate? weak var overflowDelegate: GroupCallVideoOverflowDelegate? let call: SignalCall @@ -128,8 +129,8 @@ extension GroupCallVideoOverflow: UICollectionViewDataSource { return cell } + cell.setMemberViewDelegate(memberViewDelegate) cell.configure(call: call, device: remoteDevice) - return cell } } @@ -186,4 +187,8 @@ class GroupCallVideoOverflowCell: UICollectionViewCell { func configureRemoteVideo(device: RemoteDeviceState) { memberView.configureRemoteVideo(device: device) } + + func setMemberViewDelegate(_ delegate: GroupCallMemberViewDelegate?) { + memberView.delegate = delegate + } } diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift index 156d087a6f..6c7cbe7c96 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallViewController.swift @@ -47,6 +47,11 @@ class GroupCallViewController: UIViewController { super.init(nibName: nil, bundle: nil) call.addObserverAndSyncState(observer: self) + + videoGrid.memberViewDelegate = self + videoOverflow.memberViewDelegate = self + speakerView.delegate = self + localMemberView.delegate = self } @discardableResult @@ -588,3 +593,38 @@ extension GroupCallViewController: UIScrollViewDelegate { } } } + +extension GroupCallViewController: GroupCallMemberViewDelegate { + func memberView(_ view: GroupCallMemberView, userRequestedInfoAboutError error: GroupCallMemberView.ErrorState) { + let title: String + let message: String + + switch error { + case let .blocked(address): + message = NSLocalizedString( + "GROUP_CALL_BLOCKED_ALERT_MESSAGE", + comment: "Message body for alert explaining that a group call participant is blocked") + + let titleFormat = NSLocalizedString( + "GROUP_CALL_BLOCKED_ALERT_TITLE_FORMAT", + comment: "Title for alert explaining that a group call participant is blocked. Embeds {{ user's name }}") + let displayName = contactsManager.displayName(for: address) + title = String(format: titleFormat, [displayName]) + + case let .noMediaKeys(address): + message = NSLocalizedString( + "GROUP_CALL_NO_KEYS_ALERT_MESSAGE", + comment: "Message body for alert explaining that a group call participant cannot be displayed because of missing keys") + + let titleFormat = NSLocalizedString( + "GROUP_CALL_NO_KEYS_ALERT_TITLE_FORMAT", + comment: "Title for alert explaining that a group call participant cannot be displayed because of missing keys. Embeds {{ user's name }}") + let displayName = contactsManager.displayName(for: address) + title = String(format: titleFormat, [displayName]) + } + + let actionSheet = ActionSheetController(title: title, message: message) + actionSheet.addAction(ActionSheetAction(title: CommonStrings.okButton)) + presentActionSheet(actionSheet) + } +} diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 67323dc63b..639458e269 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1522,6 +1522,12 @@ /* Message indicating that a feature can only be used by group admins. */ "GROUP_ADMIN_ONLY_WARNING" = "Only admins can change this option."; +/* Message body for alert explaining that a group call participant is blocked */ +"GROUP_CALL_BLOCKED_ALERT_MESSAGE" = "Text TBD"; + +/* Title for alert explaining that a group call participant is blocked. Embeds {{ user's name }} */ +"GROUP_CALL_BLOCKED_ALERT_TITLE_FORMAT" = "%@ is blocked"; + /* String displayed in group call grid cell when a user is blocked. Embeds {user's name} */ "GROUP_CALL_BLOCKED_USER_FORMAT" = "%@ is blocked and will not receive your audio or video"; @@ -1552,6 +1558,12 @@ /* String displayed in cell when media from a user can't be displayed in group call grid. Embeds {user's name} */ "GROUP_CALL_MISSING_MEDIA_KEYS_FORMAT" = "Can't receive audio & video from %@"; +/* Message body for alert explaining that a group call participant cannot be displayed because of missing keys */ +"GROUP_CALL_NO_KEYS_ALERT_MESSAGE" = "This may be because they have not verified your safety number change, there's a problem with their device, or they have blocked you."; + +/* Title for alert explaining that a group call participant cannot be displayed because of missing keys. Embeds {{ user's name }} */ +"GROUP_CALL_NO_KEYS_ALERT_TITLE_FORMAT" = "Can't receive audio and video from %@"; + /* Text explaining that you are the only person currently in the group call */ "GROUP_CALL_NO_ONE_HERE" = "No one else is here";