Updated the conversation creation screens
This commit is contained in:
parent
c82ee0c44b
commit
7f5b7ef703
|
@ -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()
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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 ])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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() }
|
||||
)
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue