Updated the conversation creation screens

This commit is contained in:
Morgan Pretty 2022-08-26 16:23:39 +10:00
parent c82ee0c44b
commit 7f5b7ef703
42 changed files with 639 additions and 320 deletions

View File

@ -5,6 +5,7 @@ import GRDB
import PromiseKit
import SessionUIKit
import SessionMessagingKit
import SignalUtilitiesKit
private protocol TableViewTouchDelegate {
func tableViewWasTouched(_ tableView: TableView)
@ -25,17 +26,18 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
// MARK: - Components
private lazy var nameTextField = TextField(placeholder: "vc_create_closed_group_text_field_hint".localized())
private lazy var nameTextField = TextField(
placeholder: "vc_create_closed_group_text_field_hint".localized()
)
private lazy var tableView: TableView = {
let result: TableView = TableView()
result.separatorStyle = .none
result.themeBackgroundColor = .clear
result.register(view: UserCell.self)
result.touchDelegate = self
result.dataSource = self
result.delegate = self
result.touchDelegate = self
result.separatorStyle = .none
result.backgroundColor = .clear
result.isScrollEnabled = false
result.register(view: UserCell.self)
return result
}()
@ -50,11 +52,11 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
closeButton.themeTintColor = .textPrimary
navigationItem.leftBarButtonItem = closeButton
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(createClosedGroup))
doneButton.tintColor = Colors.text
doneButton.themeTintColor = .textPrimary
navigationItem.rightBarButtonItem = doneButton
// Set up content
@ -66,7 +68,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
let explanationLabel: UILabel = UILabel()
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = "vc_create_closed_group_empty_state_message".localized()
explanationLabel.textColor = Colors.text
explanationLabel.themeTextColor = .textPrimary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
@ -88,33 +90,23 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
return
}
let mainStackView: UIStackView = UIStackView()
mainStackView.axis = .vertical
nameTextField.delegate = self
let nameTextFieldContainer: UIView = UIView()
view.addSubview(nameTextFieldContainer)
nameTextFieldContainer.pin(.top, to: .top, of: view)
nameTextFieldContainer.pin(.leading, to: .leading, of: view)
nameTextFieldContainer.pin(.trailing, to: .trailing, of: view)
nameTextFieldContainer.addSubview(nameTextField)
nameTextField.pin(.leading, to: .leading, of: nameTextFieldContainer, withInset: Values.largeSpacing)
nameTextField.pin(.top, to: .top, of: nameTextFieldContainer, withInset: Values.mediumSpacing)
nameTextFieldContainer.pin(.trailing, to: .trailing, of: nameTextField, withInset: Values.largeSpacing)
nameTextFieldContainer.pin(.bottom, to: .bottom, of: nameTextField, withInset: Values.largeSpacing)
mainStackView.addArrangedSubview(nameTextFieldContainer)
nameTextField.pin(.leading, to: .leading, of: nameTextFieldContainer, withInset: Values.largeSpacing)
nameTextField.pin(.trailing, to: .trailing, of: nameTextFieldContainer, withInset: -Values.largeSpacing)
nameTextField.pin(.bottom, to: .bottom, of: nameTextFieldContainer, withInset: -Values.largeSpacing)
let separator: UIView = UIView()
separator.backgroundColor = Colors.separator
separator.set(.height, to: Values.separatorThickness)
mainStackView.addArrangedSubview(separator)
tableView.set(.height, to: CGFloat(contactProfiles.count * 65)) // A cell is exactly 65 points high
tableView.set(.width, to: UIScreen.main.bounds.width)
mainStackView.addArrangedSubview(tableView)
let scrollView: UIScrollView = UIScrollView(wrapping: mainStackView, withInsets: UIEdgeInsets.zero)
scrollView.showsVerticalScrollIndicator = false
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.set(.width, to: UIScreen.main.bounds.width)
scrollView.pin(to: view)
view.addSubview(tableView)
tableView.pin(.top, to: .bottom, of: nameTextFieldContainer, withInset: Values.mediumSpacing)
tableView.pin(.leading, to: .leading, of: view)
tableView.pin(.trailing, to: .trailing, of: view)
tableView.pin(.bottom, to: .bottom, of: view)
}
// MARK: - Table View Data Source
@ -135,27 +127,16 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
return cell
}
// MARK: - Interaction
// MARK: - UITableViewDelegate
func textFieldDidEndEditing(_ textField: UITextField) {
crossfadeLabel.text = textField.text!.isEmpty ? NSLocalizedString("vc_create_closed_group_title", comment: "") : textField.text!
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
fileprivate func tableViewWasTouched(_ tableView: TableView) {
if nameTextField.isFirstResponder {
nameTextField.resignFirstResponder()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let nameTextFieldCenterY = nameTextField.convert(nameTextField.bounds.center, to: scrollView).y
let tableViewOriginY = tableView.convert(tableView.bounds.origin, to: scrollView).y
let titleLabelAlpha = 1 - (scrollView.contentOffset.y - nameTextFieldCenterY) / (tableViewOriginY - nameTextFieldCenterY)
let crossfadeLabelAlpha = 1 - titleLabelAlpha
navBarTitleLabel.alpha = titleLabelAlpha
crossfadeLabel.alpha = crossfadeLabelAlpha
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if !selectedContacts.contains(contactProfiles[indexPath.row].id) {
selectedContacts.insert(contactProfiles[indexPath.row].id)
@ -168,6 +149,30 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
tableView.reloadRows(at: [indexPath], with: .none)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let nameTextFieldCenterY = nameTextField.convert(nameTextField.bounds.center, to: scrollView).y
let tableViewOriginY = tableView.convert(tableView.bounds.origin, to: scrollView).y
let titleLabelAlpha = 1 - (scrollView.contentOffset.y - nameTextFieldCenterY) / (tableViewOriginY - nameTextFieldCenterY)
let crossfadeLabelAlpha = 1 - titleLabelAlpha
navBarTitleLabel.alpha = titleLabelAlpha
crossfadeLabel.alpha = crossfadeLabelAlpha
}
// MARK: - Interaction
func textFieldDidEndEditing(_ textField: UITextField) {
crossfadeLabel.text = (textField.text?.isEmpty == true ?
"vc_create_closed_group_title".localized() :
textField.text
)
}
fileprivate func tableViewWasTouched(_ tableView: TableView) {
if nameTextField.isFirstResponder {
nameTextField.resignFirstResponder()
}
}
@objc private func close() {
dismiss(animated: true, completion: nil)
}
@ -175,20 +180,20 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
@objc private func createClosedGroup() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
presentAlert(alert)
}
guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else {
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_missing_error", comment: ""))
return showError(title: "vc_create_closed_group_group_name_missing_error".localized())
}
guard name.count < 64 else {
return showError(title: NSLocalizedString("vc_create_closed_group_group_name_too_long_error", comment: ""))
return showError(title: "vc_create_closed_group_group_name_too_long_error".localized())
}
guard selectedContacts.count >= 1 else {
return showError(title: "Please pick at least 1 group member")
}
guard selectedContacts.count < 100 else { // Minus one because we're going to include self later
return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: ""))
return showError(title: "vc_create_closed_group_too_many_group_members_error".localized())
}
let selectedContacts = self.selectedContacts
let message: String? = (selectedContacts.count > 20) ? "Please wait while the group is created..." : nil
@ -211,7 +216,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
let title = "Couldn't Create Group"
let message = "Please check your internet connection and try again."
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
self?.presentAlert(alert)
}
.retainUntilComplete()

View File

@ -33,11 +33,21 @@ final class LinkPreviewView: UIView {
return result
}()
private lazy var loader: NVActivityIndicatorView = {
// FIXME: This will have issues with theme transitions
let color: UIColor = (isLightMode ? .black : .white)
private let loader: NVActivityIndicatorView = {
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: CGRect.zero,
type: .circleStrokeSpin,
color: .black,
padding: nil
)
return NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: color, padding: nil)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()
private lazy var titleLabel: UILabel = {

View File

@ -30,16 +30,22 @@ public final class VoiceMessageView: UIView {
return result
}()
private lazy var loader: NVActivityIndicatorView = {
private let loader: NVActivityIndicatorView = {
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: .zero,
type: .circleStrokeSpin,
color: Colors.text,
color: .black,
padding: nil
)
result.set(.width, to: VoiceMessageView.toggleContainerSize + 2)
result.set(.height, to: VoiceMessageView.toggleContainerSize + 2)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()

View File

@ -7,12 +7,13 @@ import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
private var pages: [UIViewController] = []
private var targetVCIndex: Int?
// MARK: Components
// MARK: - Components
private lazy var tabBar: TabBar = {
let tabs = [
TabBar.Tab(title: "vc_create_private_chat_enter_session_id_tab_title".localized()) { [weak self] in
@ -24,23 +25,26 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
}
]
return TabBar(tabs: tabs)
}()
private lazy var enterPublicKeyVC: EnterPublicKeyVC = {
let result = EnterPublicKeyVC()
result.NewDMVC = self
return result
}()
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
let result = ScanQRCodePlaceholderVC()
result.NewDMVC = self
return result
}()
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
let message = NSLocalizedString("vc_create_private_chat_scan_qr_code_explanation", comment: "")
let message = "vc_create_private_chat_scan_qr_code_explanation".localized()
let result = ScanQRCodeWrapperVC(message: message)
result.delegate = self
return result
@ -59,28 +63,33 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
super.init(nibName: nibName, bundle: bundle)
}
// MARK: Lifecycle
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setNavBarTitle("vc_create_private_chat_title".localized())
let navigationBar = navigationController!.navigationBar
// Set up navigation bar buttons
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
closeButton.themeTintColor = .textPrimary
navigationItem.leftBarButtonItem = closeButton
// Set up page VC
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
pages = [ enterPublicKeyVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
pageVC.dataSource = self
pageVC.delegate = self
pageVC.setViewControllers([ enterPublicKeyVC ], direction: .forward, animated: false, completion: nil)
// Set up tab bar
let tabBarInset: CGFloat = (UIDevice.current.isIPad ? navigationBar.height() + 20 : navigationBar.height())
view.addSubview(tabBar)
tabBar.pin(.leading, to: .leading, of: view)
let tabBarInset: CGFloat = (UIDevice.current.isIPad ? navigationBar.height() + 20 : navigationBar.height())
tabBar.pin(.top, to: .top, of: view, withInset: tabBarInset)
view.pin(.trailing, to: .trailing, of: tabBar)
// Set up page VC constraints
let pageVCView = pageVC.view!
view.addSubview(pageVCView)
@ -88,15 +97,18 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
pageVCView.pin(.top, to: .bottom, of: tabBar)
view.pin(.trailing, to: .trailing, of: pageVCView)
view.pin(.bottom, to: .bottom, of: pageVCView)
let screen = UIScreen.main.bounds
pageVCView.set(.width, to: screen.width)
let height: CGFloat = (navigationController!.view.bounds.height - navigationBar.height() - TabBar.snHeight)
pageVCView.set(.width, to: screen.width)
pageVCView.set(.height, to: height)
enterPublicKeyVC.constrainHeight(to: height)
scanQRCodePlaceholderVC.constrainHeight(to: height)
}
// MARK: General
// MARK: - General
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil }
return pages[index - 1]
@ -112,7 +124,8 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
pageVC.setViewControllers([ scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil)
}
// MARK: Updating
// MARK: - Updating
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
targetVCIndex = index
@ -123,7 +136,8 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
tabBar.selectTab(at: index)
}
// MARK: Interaction
// MARK: - Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
@ -142,36 +156,40 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
}
// This could be an ONS name
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
SnodeAPI.getSessionID(for: onsNameOrPublicKey).done { sessionID in
modalActivityIndicator.dismiss {
self?.startNewDM(with: sessionID)
}
}.catch { error in
modalActivityIndicator.dismiss {
var messageOrNil: String?
if let error = error as? SnodeAPIError {
switch error {
case .decryptionFailed, .hashingFailed, .validationFailed:
messageOrNil = error.errorDescription
default: break
ModalActivityIndicatorViewController
.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
SnodeAPI
.getSessionID(for: onsNameOrPublicKey)
.done { sessionID in
modalActivityIndicator.dismiss {
self?.startNewDM(with: sessionID)
}
}
let message: String = {
if let messageOrNil: String = messageOrNil {
return messageOrNil
.catch { error in
modalActivityIndicator.dismiss {
var messageOrNil: String?
if let error = error as? SnodeAPIError {
switch error {
case .decryptionFailed, .hashingFailed, .validationFailed:
messageOrNil = error.errorDescription
default: break
}
}
let message: String = {
if let messageOrNil: String = messageOrNil {
return messageOrNil
}
return (maybeSessionId?.prefix == .blinded ?
"You can only send messages to Blinded IDs from within an Open Group" :
"Please check the Session ID or ONS name and try again"
)
}()
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
self?.presentAlert(alert)
}
return (maybeSessionId?.prefix == .blinded ?
"You can only send messages to Blinded IDs from within an Open Group" :
"Please check the Session ID or ONS name and try again"
)
}()
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
self?.presentAlert(alert)
}
}
}
}
}
@ -188,51 +206,57 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll
}
}
private final class EnterPublicKeyVC : UIViewController {
// MARK: - EnterPublicKeyVC
private final class EnterPublicKeyVC: UIViewController {
weak var NewDMVC: NewDMVC!
private var isKeyboardShowing = false
private var simulatorWillResignFirstResponder = false
private var bottomConstraint: NSLayoutConstraint!
private let bottomMargin: CGFloat = UIDevice.current.isIPad ? Values.largeSpacing : 0
// MARK: Components
// MARK: - Components
private lazy var publicKeyTextView: TextView = {
let result = TextView(placeholder: NSLocalizedString("vc_enter_public_key_text_field_hint", comment: ""))
let result = TextView(placeholder: "vc_enter_public_key_text_field_hint".localized())
result.autocapitalizationType = .none
return result
}()
private lazy var copyButton: OutlineButton = {
let result = OutlineButton(style: .regular, size: .small)
result.setTitle("copy".lowercased(), for: UIControl.State.normal)
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
result.setTitle("copy".localized(), for: .normal)
result.addTarget(self, action: #selector(copyPublicKey), for: .touchUpInside)
return result
}()
private lazy var shareButton: OutlineButton = {
let result = OutlineButton(style: .regular, size: .small)
result.setTitle("share".lowercased(), for: UIControl.State.normal)
result.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
result.setTitle("share".localized(), for: .normal)
result.addTarget(self, action: #selector(sharePublicKey), for: .touchUpInside)
return result
}()
private lazy var nextButton: OutlineButton = {
let result = OutlineButton(style: .regular, size: .small)
result.setTitle("next".lowercased(), for: UIControl.State.normal)
result.addTarget(self, action: #selector(startNewDMIfPossible), for: UIControl.Event.touchUpInside)
let result = OutlineButton(style: .regular, size: .large)
result.setTitle("next".localized(), for: .normal)
result.addTarget(self, action: #selector(startNewDMIfPossible), for: .touchUpInside)
result.alpha = 0
return result
}()
private lazy var userPublicKeyLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = Fonts.spaceMono(ofSize: Values.mediumFontSize)
result.numberOfLines = 0
result.text = getUserHexEncodedPublicKey()
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.lineBreakMode = .byCharWrapping
result.text = getUserHexEncodedPublicKey()
result.numberOfLines = 0
return result
}()
@ -240,42 +264,53 @@ private final class EnterPublicKeyVC : UIViewController {
private lazy var spacer2 = UIView.spacer(withHeight: Values.largeSpacing)
private lazy var spacer3 = UIView.spacer(withHeight: Values.largeSpacing)
private lazy var separator = Separator(title: NSLocalizedString("your_session_id", comment: ""))
private lazy var separator = Separator(title: "your_session_id".localized())
private lazy var buttonContainer: UIStackView = {
let result = UIStackView()
result.axis = .horizontal
result.spacing = UIDevice.current.isIPad ? Values.iPadButtonSpacing : Values.mediumSpacing
result.distribution = .fillEqually
if (UIDevice.current.isIPad) {
result.layoutMargins = UIEdgeInsets(top: 0, left: Values.iPadButtonContainerMargin, bottom: 0, right: Values.iPadButtonContainerMargin)
result.isLayoutMarginsRelativeArrangement = true
}
return result
}()
// MARK: Lifecycle
// MARK: - Lifecycle
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
view.themeBackgroundColor = .clear
// User session id container
let userPublicKeyContainer = UIView(wrapping: userPublicKeyLabel, withInsets: .zero, shouldAdaptForIPadWithWidth: Values.iPadUserSessionIdContainerWidth)
let userPublicKeyContainer = UIView(
wrapping: userPublicKeyLabel,
withInsets: .zero,
shouldAdaptForIPadWithWidth: Values.iPadUserSessionIdContainerWidth
)
// Explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
explanationLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
explanationLabel.text = NSLocalizedString("vc_enter_public_key_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.text = "vc_enter_public_key_explanation".localized()
explanationLabel.themeTextColor = .textSecondary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
// Button container
buttonContainer.addArrangedSubview(copyButton)
buttonContainer.addArrangedSubview(shareButton)
let nextButtonContainer = UIView(wrapping: nextButton, withInsets: UIEdgeInsets(top: 0, leading: 80, bottom: 0, trailing: 80), shouldAdaptForIPadWithWidth: Values.iPadButtonWidth)
let nextButtonContainer = UIView(
wrapping: nextButton,
withInsets: UIEdgeInsets(top: 0, leading: 80, bottom: 0, trailing: 80),
shouldAdaptForIPadWithWidth: Values.iPadButtonWidth
)
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ publicKeyTextView, UIView.spacer(withHeight: Values.smallSpacing), explanationLabel, spacer1, separator, spacer2, userPublicKeyContainer, spacer3, buttonContainer, UIView.vStretchingSpacer(), nextButtonContainer ])
@ -306,7 +341,8 @@ private final class EnterPublicKeyVC : UIViewController {
NotificationCenter.default.removeObserver(self)
}
// MARK: General
// MARK: - General
func setSessionID(to sessionID: String){
publicKeyTextView.insertText(sessionID)
}
@ -316,62 +352,92 @@ private final class EnterPublicKeyVC : UIViewController {
}
@objc private func dismissKeyboard() {
simulatorWillResignFirstResponder = true
publicKeyTextView.resignFirstResponder()
simulatorWillResignFirstResponder = false
}
@objc private func enableCopyButton() {
copyButton.isUserInteractionEnabled = true
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle("copy".localized(), for: .normal)
}, completion: nil)
}
// MARK: Updating
// MARK: - Updating
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
#if targetEnvironment(simulator)
// Note: See 'handleKeyboardWillHideNotification' for the explanation
guard !simulatorWillResignFirstResponder else { return }
#else
guard !isKeyboardShowing else { return }
#endif
isKeyboardShowing = true
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
bottomConstraint.constant = newHeight + bottomMargin
UIView.animate(withDuration: 0.25) {
[ self.spacer1, self.separator, self.spacer2, self.userPublicKeyLabel, self.spacer3, self.buttonContainer ].forEach {
$0.alpha = 0
$0.isHidden = true
}
self.nextButton.alpha = 1
self.view.layoutIfNeeded()
}
}
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
#if targetEnvironment(simulator)
// Note: On the simulator the keyboard won't appear by default (unless you enable
// it) this results in the "keyboard will hide" notification incorrectly getting
// triggered immediately - the 'simulatorWillResignFirstResponder' value is a workaround
// to make this behave more like a real device when testing
guard isKeyboardShowing && simulatorWillResignFirstResponder else { return }
#else
guard isKeyboardShowing else { return }
#endif
isKeyboardShowing = false
bottomConstraint.constant = bottomMargin
UIView.animate(withDuration: 0.25) {
[ self.spacer1, self.separator, self.spacer2, self.userPublicKeyLabel, self.spacer3, self.buttonContainer ].forEach {
$0.alpha = 1
$0.isHidden = false
}
self.nextButton.alpha = (self.publicKeyTextView.text.isEmpty ? 0 : 1)
self.view.layoutIfNeeded()
}
}
// MARK: Interaction
// MARK: - Interaction
@objc private func copyPublicKey() {
UIPasteboard.general.string = getUserHexEncodedPublicKey()
copyButton.isUserInteractionEnabled = false
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("copied", comment: ""), for: UIControl.State.normal)
self.copyButton.setTitle("copied".localized(), for: .normal)
}, completion: nil)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
}
@objc private func sharePublicKey() {
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
NewDMVC.navigationController!.present(shareVC, animated: true, completion: nil)
}
@ -381,36 +447,43 @@ private final class EnterPublicKeyVC : UIViewController {
}
}
private final class ScanQRCodePlaceholderVC : UIViewController {
// MARK: - ScanQRCodePlaceholderVC
private final class ScanQRCodePlaceholderVC: UIViewController {
weak var NewDMVC: NewDMVC!
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
view.themeBackgroundColor = .clear
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("vc_scan_qr_code_camera_access_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.text = "vc_scan_qr_code_camera_access_explanation".localized()
explanationLabel.themeTextColor = .textPrimary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
// Set up call to action button
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitle("vc_scan_qr_code_grant_camera_access_button_title".localized(), for: UIControl.State.normal)
callToActionButton.setThemeTitleColor(.primary, for: .normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
stackView.axis = .vertical
stackView.spacing = Values.mediumSpacing
stackView.alignment = .center
// Set up constraints
view.set(.width, to: UIScreen.main.bounds.width)
view.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
let verticalCenteringConstraint = stackView.center(.vertical, in: view)
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
}

View File

@ -17,11 +17,22 @@ class EmptySearchResultCell: UITableViewCell {
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
private let spinner: NVActivityIndicatorView = {
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: CGRect.zero,
type: .circleStrokeSpin,
color: .black,
padding: nil
)
result.set(.width, to: 40)
result.set(.height, to: 40)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()

View File

@ -59,6 +59,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
internal lazy var tableView: UITableView = {
let result: UITableView = UITableView(frame: .zero, style: .grouped)
result.themeBackgroundColor = .clear
result.rowHeight = UITableView.automaticDimension
result.estimatedRowHeight = 60
result.separatorStyle = .none

View File

@ -73,7 +73,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
private lazy var tableView: UITableView = {
let result = UITableView()
result.separatorStyle = .none
result.backgroundColor = .clear
result.themeBackgroundColor = .clear
result.contentInset = UIEdgeInsets(
top: 0,
left: 0,

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -702,10 +702,10 @@
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one conversations.";
"PRIVACY_SECTION_TYPING_INDICATORS" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_TITLE" = "Typing Indicators";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one chats.";
"PRIVACY_TYPING_INDICATORS_DESCRIPTION" = "See and share typing indicators in one-to-one conversations.";
"PRIVACY_SECTION_LINK_PREVIEWS" = "Link Previews";
"PRIVACY_LINK_PREVIEWS_TITLE" = "Send Link Previews";
"PRIVACY_LINK_PREVIEWS_DESCRIPTION" = "Generate link previews for supported URLs.";

View File

@ -60,7 +60,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
// Navigation bar buttons
let navBarHeight: CGFloat = (navigationController?.navigationBar.frame.size.height ?? 0)
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
closeButton.themeTintColor = .textPrimary
navigationItem.leftBarButtonItem = closeButton
// Page VC
@ -190,13 +190,20 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
// MARK: - Convenience
private func showError(title: String, message: String = "") {
let alert: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
presentAlert(alert)
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: title,
explanation: message,
cancelTitle: "BUTTON_OK".localized(),
cancelStyle: .textPrimary
)
)
self.navigationController?.present(confirmationModal, animated: true, completion: nil)
}
}
// MARK: - EnterURLVC
private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate {
weak var joinOpenGroupVC: JoinOpenGroupVC?
@ -220,7 +227,7 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
result.setContentHuggingPriority(.required, for: .vertical)
result.font = .boldSystemFont(ofSize: Values.largeFontSize)
result.text = "vc_join_open_group_suggestions_title".localized()
result.textColor = Colors.text
result.themeTextColor = .textPrimary
result.lineBreakMode = .byWordWrapping
result.numberOfLines = 0
@ -239,7 +246,7 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
view.themeBackgroundColor = .clear
// Next button
let nextButton = OutlineButton(style: .regular, size: .large)
@ -374,23 +381,23 @@ private final class ScanQRCodePlaceholderVC: UIViewController {
override func viewDidLoad() {
// Remove background color
view.backgroundColor = .clear
view.themeBackgroundColor = .clear
// Explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("vc_scan_qr_code_camera_access_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.text = "vc_scan_qr_code_camera_access_explanation".localized()
explanationLabel.themeTextColor = .textPrimary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
// Call to action button
let callToActionButton = UIButton()
callToActionButton.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
callToActionButton.setTitle(NSLocalizedString("vc_scan_qr_code_grant_camera_access_button_title", comment: ""), for: UIControl.State.normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: UIControl.Event.touchUpInside)
callToActionButton.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize)
callToActionButton.setTitle("vc_scan_qr_code_grant_camera_access_button_title".localized(), for: .normal)
callToActionButton.setThemeTitleColor(.primary, for: .normal)
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: .touchUpInside)
// Stack view
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])

View File

@ -7,34 +7,50 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
private let maxWidth: CGFloat
private var rooms: [OpenGroupAPI.Room] = [] { didSet { update() } }
private var heightConstraint: NSLayoutConstraint!
var delegate: OpenGroupSuggestionGridDelegate?
// MARK: - UI
private static let cellHeight: CGFloat = 40
private static let separatorWidth = 1 / UIScreen.main.scale
private static let separatorWidth = Values.separatorThickness
private static let numHorizontalCells: CGFloat = (UIDevice.current.isIPad ? 4 : 2)
private lazy var layout: UICollectionViewFlowLayout = {
let result = UICollectionViewFlowLayout()
result.minimumLineSpacing = 0
result.minimumInteritemSpacing = 0
private lazy var layout: LastRowCenteredLayout = {
let result = LastRowCenteredLayout()
result.minimumLineSpacing = Values.mediumSpacing
result.minimumInteritemSpacing = Values.mediumSpacing
return result
}()
private lazy var collectionView: UICollectionView = {
let result = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
result.register(Cell.self, forCellWithReuseIdentifier: Cell.identifier)
result.backgroundColor = .clear
let result = UICollectionView(frame: .zero, collectionViewLayout: layout)
result.themeBackgroundColor = .clear
result.isScrollEnabled = false
result.register(view: Cell.self)
result.dataSource = self
result.delegate = self
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
private let spinner: NVActivityIndicatorView = {
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: CGRect.zero,
type: .circleStrokeSpin,
color: .black,
padding: nil
)
result.set(.width, to: OpenGroupSuggestionGrid.cellHeight)
result.set(.height, to: OpenGroupSuggestionGrid.cellHeight)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()
@ -47,16 +63,16 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
private lazy var errorImageView: UIImageView = {
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "warning").withRenderingMode(.alwaysTemplate))
result.tintColor = Colors.destructive
result.themeTintColor = .danger
return result
}()
private lazy var errorTitleLabel: UILabel = {
let result: UILabel = UILabel()
result.font = UIFont.systemFont(ofSize: Values.mediumFontSize, weight: .medium)
result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .medium)
result.text = "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE".localized()
result.textColor = Colors.text
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.numberOfLines = 0
@ -65,9 +81,9 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
private lazy var errorSubtitleLabel: UILabel = {
let result: UILabel = UILabel()
result.font = UIFont.systemFont(ofSize: Values.smallFontSize, weight: .medium)
result.font = .systemFont(ofSize: Values.smallFontSize, weight: .medium)
result.text = "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE".localized()
result.textColor = Colors.text
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.numberOfLines = 0
@ -78,7 +94,9 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
init(maxWidth: CGFloat) {
self.maxWidth = maxWidth
super.init(frame: CGRect.zero)
initialize()
}
@ -134,8 +152,10 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
private func update() {
spinner.stopAnimating()
spinner.isHidden = true
let roomCount = min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2)
let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(roomCount) / 2)
let roomCount: CGFloat = CGFloat(min(rooms.count, 8)) // Cap to a maximum of 8 (4 rows of 2)
let numRows: CGFloat = ceil(roomCount / OpenGroupSuggestionGrid.numHorizontalCells)
let height: CGFloat = ((OpenGroupSuggestionGrid.cellHeight * numRows) + ((numRows - 1) * layout.minimumLineSpacing))
heightConstraint.constant = height
collectionView.reloadData()
errorView.isHidden = (roomCount > 0)
@ -144,8 +164,20 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
// MARK: - Layout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellWidth = UIDevice.current.isIPad ? maxWidth / 4 : maxWidth / 2
return CGSize(width: cellWidth, height: OpenGroupSuggestionGrid.cellHeight)
guard
indexPath.item == (collectionView.numberOfItems(inSection: indexPath.section) - 1) &&
indexPath.item % 2 == 0
else {
let cellWidth: CGFloat = ((maxWidth / OpenGroupSuggestionGrid.numHorizontalCells) - ((OpenGroupSuggestionGrid.numHorizontalCells - 1) * layout.minimumInteritemSpacing))
return CGSize(width: cellWidth, height: OpenGroupSuggestionGrid.cellHeight)
}
// If the last item is by itself then we want to make it wider
return CGSize(
width: (Cell.calculatedWith(for: rooms[indexPath.item].name)),
height: OpenGroupSuggestionGrid.cellHeight
)
}
// MARK: - Data Source
@ -155,8 +187,9 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell
let cell: Cell = collectionView.dequeue(type: Cell.self, for: indexPath)
cell.room = rooms[indexPath.item]
return cell
}
@ -171,72 +204,95 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
// MARK: - Cell
extension OpenGroupSuggestionGrid {
fileprivate final class Cell : UICollectionViewCell {
fileprivate final class Cell: UICollectionViewCell {
private static let labelFont: UIFont = .systemFont(ofSize: Values.smallFontSize)
private static let imageSize: CGFloat = 30
private static let itemPadding: CGFloat = Values.smallSpacing
private static let contentLeftPadding: CGFloat = 7
private static let contentRightPadding: CGFloat = Values.veryLargeSpacing
fileprivate static func calculatedWith(for title: String) -> CGFloat {
// FIXME: Do the calculations properly in the 'LastRowCenteredLayout' to handle imageless cells
return (
contentLeftPadding +
imageSize +
itemPadding +
NSAttributedString(string: title, attributes: [ .font: labelFont ]).size().width +
contentRightPadding
)
}
var room: OpenGroupAPI.Room? { didSet { update() } }
static let identifier = "OpenGroupSuggestionGridCell"
private lazy var snContentView: UIView = {
let result = UIView()
result.backgroundColor = Colors.navigationBarBackground
result.set(.height, to: Cell.contentViewHeight)
let result: UIView = UIView()
result.themeBorderColor = .borderSeparator
result.layer.cornerRadius = Cell.contentViewCornerRadius
result.layer.borderWidth = 1
result.set(.height, to: Cell.contentViewHeight)
return result
}()
private lazy var imageView: UIImageView = {
let result = UIImageView()
let size: CGFloat = 24
result.set(.width, to: size)
result.set(.height, to: size)
result.layer.cornerRadius = size / 2
let result: UIImageView = UIImageView()
result.set(.width, to: Cell.imageSize)
result.set(.height, to: Cell.imageSize)
result.layer.cornerRadius = (Cell.imageSize / 2)
result.clipsToBounds = true
return result
}()
private lazy var label: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.smallFontSize)
let result: UILabel = UILabel()
result.font = Cell.labelFont
result.themeTextColor = .textPrimary
result.lineBreakMode = .byTruncatingTail
return result
}()
private static let contentViewInset: CGFloat = 4
private static let contentViewInset: CGFloat = 0
private static var contentViewHeight: CGFloat { OpenGroupSuggestionGrid.cellHeight - 2 * contentViewInset }
private static var contentViewCornerRadius: CGFloat { contentViewHeight / 2 }
override init(frame: CGRect) {
super.init(frame: frame)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
themeBackgroundColor = .backgroundPrimary
selectedBackgroundView = UIView()
selectedBackgroundView?.themeBackgroundColor = .backgroundSecondary
selectedBackgroundView?.layer.cornerRadius = Cell.contentViewCornerRadius
addSubview(snContentView)
let stackView = UIStackView(arrangedSubviews: [ imageView, label ])
stackView.axis = .horizontal
stackView.spacing = Values.smallSpacing
stackView.spacing = Cell.itemPadding
snContentView.addSubview(stackView)
stackView.center(.vertical, in: snContentView)
stackView.pin(.leading, to: .leading, of: snContentView, withInset: 4)
snContentView.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: Values.smallSpacing).isActive = true
snContentView.pin(to: self, withInset: Cell.contentViewInset)
}
override func layoutSubviews() {
super.layoutSubviews()
let newPath = UIBezierPath(roundedRect: snContentView.bounds, cornerRadius: Cell.contentViewCornerRadius).cgPath
snContentView.layer.shadowPath = newPath
snContentView.layer.shadowColor = UIColor.black.cgColor
snContentView.layer.shadowOffset = CGSize.zero
snContentView.layer.shadowOpacity = isLightMode ? 0.2 : 0.6
snContentView.layer.shadowRadius = 2
stackView.pin(.leading, to: .leading, of: snContentView, withInset: Cell.contentLeftPadding)
snContentView.trailingAnchor
.constraint(
greaterThanOrEqualTo: stackView.trailingAnchor,
constant: Cell.contentRightPadding
)
.isActive = true
snContentView.pin(to: self)
}
private func update() {
@ -277,3 +333,26 @@ extension OpenGroupSuggestionGrid {
protocol OpenGroupSuggestionGridDelegate {
func join(_ room: OpenGroupAPI.Room)
}
// MARK: - LastRowCenteredLayout
class LastRowCenteredLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// If we have an odd number of items then we want to center the last one horizontally
let elementAttributes: [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElements(in: rect)
guard
(elementAttributes?.count ?? 0) % 2 == 1,
let lastItemAttributes: UICollectionViewLayoutAttributes = elementAttributes?.last
else { return elementAttributes }
lastItemAttributes.frame = CGRect(
x: ((rect.width - lastItemAttributes.frame.size.width) / 2),
y: lastItemAttributes.frame.origin.y,
width: lastItemAttributes.frame.size.width,
height: lastItemAttributes.frame.size.height
)
return elementAttributes
}
}

View File

@ -19,11 +19,22 @@ final class PathVC: BaseVC {
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
private let spinner: NVActivityIndicatorView = {
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: CGRect.zero,
type: .circleStrokeSpin,
color: .black,
padding: nil
)
result.set(.width, to: 64)
result.set(.height, to: 64)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()

View File

@ -143,6 +143,7 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
explanation: "PRIVACY_CALLS_WARNING_DESCRIPTION".localized(),
stateToShow: .whenDisabled,
confirmTitle: "continue_2".localized(),
confirmStyle: .textPrimary
) { requestMicrophonePermissionIfNeeded() }
)

View File

@ -20,12 +20,30 @@ final class UserCell: UITableViewCell {
private lazy var displayNameLabel: UILabel = {
let result: UILabel = UILabel()
result.textColor = Colors.text
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
result.lineBreakMode = .byTruncatingTail
return result
}()
private let spacer: UIView = {
let result: UIView = UIView.hStretchingSpacer()
result.widthAnchor
.constraint(greaterThanOrEqualToConstant: Values.mediumSpacing)
.isActive = true
return result
}()
private let selectionView: RadioButton = {
let result: RadioButton = RadioButton(size: .medium)
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .bold)
return result
}()
private lazy var accessoryImageView: UIImageView = {
let result: UIImageView = UIImageView()
@ -38,7 +56,7 @@ final class UserCell: UITableViewCell {
private lazy var separator: UIView = {
let result: UIView = UIView()
result.backgroundColor = Colors.separator
result.themeBackgroundColor = .borderSeparator
result.set(.height, to: Values.separatorThickness)
return result
@ -58,11 +76,11 @@ final class UserCell: UITableViewCell {
private func setUpViewHierarchy() {
// Background color
backgroundColor = Colors.cellBackground
themeBackgroundColor = .conversationButton_background
// Highlight color
let selectedBackgroundView = UIView()
selectedBackgroundView.backgroundColor = .clear // Disabled for now
selectedBackgroundView.themeBackgroundColor = .clear // Disabled for now
self.selectedBackgroundView = selectedBackgroundView
// Profile picture image view
@ -72,15 +90,14 @@ final class UserCell: UITableViewCell {
profilePictureView.size = profilePictureViewSize
// Main stack view
let spacer = UIView.hStretchingSpacer()
spacer.widthAnchor.constraint(greaterThanOrEqualToConstant: Values.mediumSpacing).isActive = true
let stackView = UIStackView(
arrangedSubviews: [
profilePictureView,
UIView.hSpacer(Values.mediumSpacing),
displayNameLabel,
spacer,
accessoryImageView
accessoryImageView,
selectionView
]
)
stackView.axis = .horizontal
@ -134,26 +151,42 @@ final class UserCell: UITableViewCell {
)
switch accessory {
case .none: accessoryImageView.isHidden = true
case .none:
selectionView.isHidden = true
accessoryImageView.isHidden = true
displayNameLabel.isHidden = false
spacer.isHidden = false
case .lock:
selectionView.isHidden = true
accessoryImageView.isHidden = false
accessoryImageView.image = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate)
accessoryImageView.tintColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
accessoryImageView.themeTintColor = .textPrimary
accessoryImageView.alpha = Values.mediumOpacity
displayNameLabel.isHidden = false
spacer.isHidden = false
case .tick(let isSelected):
let icon: UIImage = (isSelected ? #imageLiteral(resourceName: "CircleCheck") : #imageLiteral(resourceName: "Circle"))
accessoryImageView.isHidden = false
accessoryImageView.image = icon.withRenderingMode(.alwaysTemplate)
accessoryImageView.tintColor = Colors.text
selectionView.isHidden = false
selectionView.text = displayNameLabel.text
selectionView.update(isSelected: isSelected)
accessoryImageView.isHidden = true
displayNameLabel.isHidden = true
spacer.isHidden = true
case .x:
selectionView.isHidden = true
accessoryImageView.isHidden = false
accessoryImageView.image = #imageLiteral(resourceName: "X").withRenderingMode(.alwaysTemplate)
accessoryImageView.contentMode = .center
accessoryImageView.tintColor = Colors.text
accessoryImageView.themeTintColor = .textPrimary
accessoryImageView.alpha = 1
displayNameLabel.isHidden = false
spacer.isHidden = false
}
let alpha: CGFloat = (isZombie ? 0.5 : 1)
[ profilePictureView, displayNameLabel, accessoryImageView ].forEach { $0.alpha = alpha }
[ profilePictureView, displayNameLabel, accessoryImageView, selectionView ]
.forEach { $0.alpha = alpha }
}
}

View File

@ -22,7 +22,7 @@ public class ConfirmationModal: Modal {
let title: String
let explanation: String?
let stateToShow: State
let confirmTitle: String
let confirmTitle: String?
let confirmStyle: ThemeValue
let cancelTitle: String
let cancelStyle: ThemeValue
@ -32,7 +32,7 @@ public class ConfirmationModal: Modal {
title: String,
explanation: String? = nil,
stateToShow: State = .always,
confirmTitle: String = "continue_2".localized(),
confirmTitle: String? = nil,
confirmStyle: ThemeValue = .textPrimary,
cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
cancelStyle: ThemeValue = .danger,
@ -140,9 +140,9 @@ public class ConfirmationModal: Modal {
// MARK: - Lifecycle
init(info: Info, onConfirm: @escaping (UIViewController) -> ()) {
init(info: Info, onConfirm: ((UIViewController) -> ())? = nil) {
self.onConfirm = { viewController in
onConfirm(viewController)
onConfirm?(viewController)
info.onConfirm?()
}
@ -157,6 +157,7 @@ public class ConfirmationModal: Modal {
explanationLabel.isHidden = (info.explanation == nil)
confirmButton.setTitle(info.confirmTitle, for: .normal)
confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal)
confirmButton.isHidden = (info.confirmTitle == nil)
cancelButton.setTitle(info.cancelTitle, for: .normal)
cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal)
}

View File

@ -10,9 +10,8 @@ public final class Separator: UIView {
private lazy var titleLabel: UILabel = {
let result = UILabel()
result.font = .systemFont(ofSize: Values.smallFontSize)
result.themeTextColor = .textPrimary
result.themeTextColor = .textSecondary
result.textAlignment = .center
result.alpha = Values.mediumOpacity
return result
}()
@ -20,7 +19,7 @@ public final class Separator: UIView {
private lazy var lineLayer: CAShapeLayer = {
let result = CAShapeLayer()
result.lineWidth = Values.separatorThickness
result.themeStrokeColor = .borderSeparator
result.themeStrokeColor = .textSecondary
result.themeFillColor = .clear
return result

View File

@ -1,7 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
@objc(SNTextField)
public final class TextField : UITextField {
public final class TextField: UITextField {
private let usesDefaultHeight: Bool
private let height: CGFloat
private let horizontalInset: CGFloat
@ -20,8 +22,11 @@ public final class TextField : UITextField {
self.height = customHeight ?? TextField.height
self.horizontalInset = customHorizontalInset ?? (isIPhone5OrSmaller ? Values.mediumSpacing : Values.largeSpacing)
self.verticalInset = customVerticalInset ?? (isIPhone5OrSmaller ? Values.smallSpacing : Values.largeSpacing)
super.init(frame: CGRect.zero)
self.placeholder = placeholder
setUpStyle()
}
@ -34,26 +39,37 @@ public final class TextField : UITextField {
}
private func setUpStyle() {
textColor = Colors.text
font = .systemFont(ofSize: Values.smallFontSize)
let placeholder = NSMutableAttributedString(string: self.placeholder!)
let placeholderColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
placeholder.addAttribute(.foregroundColor, value: placeholderColor, range: NSRange(location: 0, length: placeholder.length))
attributedPlaceholder = placeholder
tintColor = Colors.accent
keyboardAppearance = isLightMode ? .light : .dark
themeTextColor = .textPrimary
themeTintColor = .primary
themeBorderColor = .borderSeparator
layer.borderWidth = 1
layer.cornerRadius = TextField.cornerRadius
if usesDefaultHeight {
set(.height, to: height)
}
layer.borderColor = isLightMode ? Colors.text.cgColor : Colors.border.withAlphaComponent(Values.lowOpacity).cgColor
layer.borderWidth = 1
layer.cornerRadius = TextField.cornerRadius
ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in
switch theme.interfaceStyle {
case .light: self?.keyboardAppearance = .light
default: self?.keyboardAppearance = .dark
}
if let textSecondary: UIColor = theme.colors[.textSecondary], let placeholder: String = self?.placeholder {
self?.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [ .foregroundColor: textSecondary]
)
}
}
}
public override func textRect(forBounds bounds: CGRect) -> CGRect {
if usesDefaultHeight {
return bounds.insetBy(dx: horizontalInset, dy: verticalInset)
} else {
}
else {
return bounds.insetBy(dx: Values.mediumSpacing, dy: Values.smallSpacing)
}
}
@ -61,7 +77,8 @@ public final class TextField : UITextField {
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
if usesDefaultHeight {
return bounds.insetBy(dx: horizontalInset, dy: verticalInset)
} else {
}
else {
return bounds.insetBy(dx: Values.mediumSpacing, dy: Values.smallSpacing)
}
}

View File

@ -12,18 +12,27 @@ public final class TextView : UITextView, UITextViewDelegate {
private lazy var placeholderLabel: UILabel = {
let result = UILabel()
result.font = .systemFont(ofSize: Values.smallFontSize)
result.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
result.themeTextColor = .textSecondary
return result
}()
public init(placeholder: String, usesDefaultHeight: Bool = true, customHeight: CGFloat? = nil, customHorizontalInset: CGFloat? = nil, customVerticalInset: CGFloat? = nil) {
public init(
placeholder: String,
usesDefaultHeight: Bool = true,
customHeight: CGFloat? = nil,
customHorizontalInset: CGFloat? = nil,
customVerticalInset: CGFloat? = nil
) {
self.usesDefaultHeight = usesDefaultHeight
self.height = customHeight ?? TextField.height
self.horizontalInset = customHorizontalInset ?? (isIPhone5OrSmaller ? Values.mediumSpacing : Values.largeSpacing)
self.verticalInset = customVerticalInset ?? (isIPhone5OrSmaller ? Values.smallSpacing : Values.largeSpacing)
self.placeholder = placeholder
super.init(frame: CGRect.zero, textContainer: nil)
self.delegate = self
setUpStyle()
}
@ -38,18 +47,21 @@ public final class TextView : UITextView, UITextViewDelegate {
private func setUpStyle() {
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
placeholderLabel.text = placeholder
backgroundColor = .clear
textColor = Colors.text
font = .systemFont(ofSize: Values.smallFontSize)
tintColor = Colors.accent
keyboardAppearance = isLightMode ? .light : .dark
themeBackgroundColor = .clear
themeTextColor = .textPrimary
themeTintColor = .primary
themeBorderColor = .borderSeparator
layer.borderWidth = 1
layer.cornerRadius = TextField.cornerRadius
placeholderLabel.text = placeholder
if usesDefaultHeight {
set(.height, to: height)
}
layer.borderColor = isLightMode ? Colors.text.cgColor : Colors.border.withAlphaComponent(Values.lowOpacity).cgColor
layer.borderWidth = 1
layer.cornerRadius = TextField.cornerRadius
let horizontalInset = usesDefaultHeight ? self.horizontalInset : Values.mediumSpacing
textContainerInset = UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset)
addSubview(placeholderLabel)
@ -57,6 +69,13 @@ public final class TextView : UITextView, UITextViewDelegate {
placeholderLabel.pin(.top, to: .top, of: self)
pin(.trailing, to: .trailing, of: placeholderLabel, withInset: horizontalInset)
pin(.bottom, to: .bottom, of: placeholderLabel)
ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in
switch theme.interfaceStyle {
case .light: self?.keyboardAppearance = .light
default: self?.keyboardAppearance = .dark
}
}
}
public func textViewDidChange(_ textView: UITextView) {

View File

@ -18,7 +18,7 @@ public final class Values : NSObject {
@objc public static let massiveFontSize = CGFloat(50)
// MARK: - Element Sizes
@objc public static let smallButtonHeight = isIPhone5OrSmaller ? CGFloat(24) : CGFloat(27)
@objc public static let smallButtonHeight = isIPhone5OrSmaller ? CGFloat(24) : CGFloat(28)
@objc public static let mediumButtonHeight = isIPhone5OrSmaller ? CGFloat(30) : CGFloat(34)
@objc public static let largeButtonHeight = isIPhone5OrSmaller ? CGFloat(40) : CGFloat(45)
@objc public static let alertButtonHeight: CGFloat = 50

View File

@ -136,12 +136,23 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
return stackView
}()
private lazy var loadingView: NVActivityIndicatorView = {
let view: NVActivityIndicatorView = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
private let loadingView: NVActivityIndicatorView = {
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: CGRect.zero,
type: .circleStrokeSpin,
color: .black,
padding: nil
)
result.translatesAutoresizingMaskIntoConstraints = false
result.isHidden = true
return view
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()
private lazy var imageView: UIImageView = {

View File

@ -1,6 +1,4 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import MediaPlayer
@ -9,25 +7,49 @@ import NVActivityIndicatorView
// A modal view that be used during blocking interactions (e.g. waiting on response from
// service or on the completion of a long-running local operation).
@objc
public class ModalActivityIndicatorViewController: OWSViewController {
let canCancel: Bool
let message: String?
@objc
public var wasCancelled: Bool = false
lazy var dimmingView: UIView = {
let result = UIVisualEffectView()
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
result?.effect = UIBlurEffect(
style: (theme.interfaceStyle == .light ?
UIBlurEffect.Style.systemUltraThinMaterialLight :
UIBlurEffect.Style.systemUltraThinMaterial
)
)
}
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: .white, padding: nil)
let result: NVActivityIndicatorView = NVActivityIndicatorView(
frame: CGRect.zero,
type: .circleStrokeSpin,
color: .white,
padding: nil
)
result.set(.width, to: 64)
result.set(.height, to: 64)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
result?.color = textPrimary
}
return result
}()
var wasDimissed: Bool = false
// MARK: Initializers
// MARK: - Initializers
@available(*, unavailable, message:"use other constructor instead.")
public required init?(coder aDecoder: NSCoder) {
@ -40,7 +62,6 @@ public class ModalActivityIndicatorViewController: OWSViewController {
super.init(nibName: nil, bundle: nil)
}
@objc
public class func present(
fromViewController: UIViewController?,
canCancel: Bool = false,
@ -52,9 +73,11 @@ public class ModalActivityIndicatorViewController: OWSViewController {
AssertIsOnMainThread()
let view = ModalActivityIndicatorViewController(canCancel: canCancel, message: message)
// Present this modal _over_ the current view contents.
view.modalPresentationStyle = .overFullScreen
view.modalTransitionStyle = .crossDissolve
fromViewController.present(view, animated: false) {
DispatchQueue.global().async {
backgroundBlock(view)
@ -62,7 +85,6 @@ public class ModalActivityIndicatorViewController: OWSViewController {
}
}
@objc
public func dismiss(completion: @escaping () -> Void) {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
@ -75,7 +97,8 @@ public class ModalActivityIndicatorViewController: OWSViewController {
// Only dismiss once.
self.dismiss(animated: false, completion: completion)
wasDimissed = true
} else {
}
else {
// If already dismissed, wait a beat then call completion.
DispatchQueue.main.async {
completion()
@ -86,41 +109,48 @@ public class ModalActivityIndicatorViewController: OWSViewController {
public override func loadView() {
super.loadView()
self.view.backgroundColor = UIColor(white: 0, alpha: 0.6)
self.view.isOpaque = false
self.view.themeBackgroundColor = .clear
self.view.addSubview(dimmingView)
dimmingView.pin(to: self.view)
if let message = message {
let messageLabel = UILabel()
messageLabel.text = message
let messageLabel: UILabel = UILabel()
messageLabel.font = .systemFont(ofSize: Values.mediumFontSize)
messageLabel.textColor = UIColor.white
messageLabel.text = message
messageLabel.themeTextColor = .textPrimary
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
let stackView = UIStackView(arrangedSubviews: [ messageLabel, spinner ])
stackView.axis = .vertical
stackView.spacing = Values.largeSpacing
stackView.alignment = .center
self.view.addSubview(stackView)
stackView.center(in: self.view)
} else {
}
else {
self.view.addSubview(spinner)
spinner.autoCenterInSuperview()
}
if canCancel {
let cancelButton = UIButton(type: .custom)
let cancelButton: UIButton = UIButton(type: .custom)
cancelButton.setTitle(CommonStrings.cancelButton, for: .normal)
cancelButton.setTitleColor(UIColor.white, for: .normal)
cancelButton.setThemeTitleColor(.textPrimary, for: .normal)
cancelButton.backgroundColor = UIColor.ows_darkGray
cancelButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: ScaleFromIPhone5To7Plus(18, 22))
cancelButton.layer.cornerRadius = ScaleFromIPhone5To7Plus(4, 5)
cancelButton.clipsToBounds = true
cancelButton.addTarget(self, action: #selector(cancelPressed), for: .touchUpInside)
let buttonWidth = ScaleFromIPhone5To7Plus(140, 160)
let buttonHeight = ScaleFromIPhone5To7Plus(40, 50)
self.view.addSubview(cancelButton)
cancelButton.autoHCenterInSuperview()
cancelButton.autoPinEdge(toSuperviewEdge: .bottom, withInset: 50)
cancelButton.autoSetDimension(.width, toSize: buttonWidth)

View File

@ -1,30 +1,35 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import SessionUIKit
public extension UIView {
static func hSpacer(_ width: CGFloat) -> UIView {
let result = UIView()
let result: UIView = UIView()
result.set(.width, to: width)
return result
}
static func vSpacer(_ height: CGFloat) -> UIView {
let result = UIView()
let result: UIView = UIView()
result.set(.height, to: height)
return result
}
static func vhSpacer(_ width: CGFloat, _ height: CGFloat) -> UIView {
let result = UIView()
let result: UIView = UIView()
result.set(.width, to: width)
result.set(.height, to: height)
return result
}
static func separator() -> UIView {
let result = UIView()
let result: UIView = UIView()
result.set(.height, to: Values.separatorThickness)
result.themeBackgroundColor = .borderSeparator
return result
}
}