mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
The `SessionThreadViewModel` has been renamed to `ConversationInfoViewModel` and is now a simple struct (as opposed to a FetchableRecord with a handful of behemoth database queries) The `ConversationInfoViewModel` and `MessageViewModel` now get initialised with a `ConversationDataCache` which is an object that holds a bunch of [ID -> Data] stores for various types that are needed to construct the aforementioned view models. The `ConversationDataCache` will generally be populated via the `ConversationDataHelper` which, when combined with our `ObservableEvent` system, is where the majority of the work for this refactoring has gone. This helper essentially has 4 parts: - `determineFetchRequirements`: Reviews any events which have come through and determines whether anything needs to be fetched from the database - `applyNonDatabaseEvents`: Updates the cached data directly based on delta information included in the `ObservableEvent` instances - `fetchFromDatabase`: Fetches any data from the database based on the info returned from `determineFetchRequirements`, this function will also resolve any dependant data that newly fetched objects need if they aren't already in the cache - `fetchFromLibSession`: Retrieves requested data from libSession Using the above setup all screens which contain conversation or message data now use these two view models, and can share the state of the view models between each other to reduce the need to refetch data when entering a new screen Some additional changes included due to the above are: • No longer need to pass `displayNameRetriever` throughout • Removed deprecated display name retrieval functions • Removed some duplicate code • Removed some synchronous database query hacks we needed until refactoring had been completed • Refactored mentions to be handled by tags • Refactored and optimised the searching logic (also added some caching) • Refactored the ThreadSettingsViewModel to use the new ObservationBuilder system • Refactored the ThreadPickerViewModel to use the new ObservationBuilder system • Refactored the HomeViewModel to retrieve data using the `ConversationDataHelper` • Refactored the ConversationViewModel to retrieve data using the `ConversationDataHelper` • Fixed an issue where communities weren't getting the correct "joined" timestamp
666 lines
26 KiB
Swift
666 lines
26 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import Combine
|
|
import AVFoundation
|
|
import GRDB
|
|
import SessionUIKit
|
|
import SessionMessagingKit
|
|
import SessionNetworkingKit
|
|
import SessionUtilitiesKit
|
|
import SignalUtilitiesKit
|
|
|
|
final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate, NavigatableStateHolder {
|
|
private let dependencies: Dependencies
|
|
public let navigatableState: NavigatableState = NavigatableState()
|
|
private var disposables: Set<AnyCancellable> = Set()
|
|
|
|
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
|
private var pages: [UIViewController] = []
|
|
private var isJoining = false
|
|
private var targetVCIndex: Int?
|
|
|
|
// MARK: - Components
|
|
|
|
private lazy var tabBar: TabBar = {
|
|
let tabs: [TabBar.Tab] = [
|
|
TabBar.Tab(title: "communityUrl".localized()) { [weak self] in
|
|
guard let self = self else { return }
|
|
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
|
},
|
|
TabBar.Tab(title: "qrScan".localized()) { [weak self] in
|
|
guard let self = self else { return }
|
|
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
|
|
}
|
|
]
|
|
|
|
return TabBar(tabs: tabs)
|
|
}()
|
|
|
|
private lazy var enterURLVC: EnterURLVC = {
|
|
let result: EnterURLVC = EnterURLVC(using: dependencies)
|
|
result.joinOpenGroupVC = self
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = {
|
|
let result: ScanQRCodePlaceholderVC = ScanQRCodePlaceholderVC(using: dependencies)
|
|
result.joinOpenGroupVC = self
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var scanQRCodeWrapperVC: ScanQRCodeWrapperVC = {
|
|
let result: ScanQRCodeWrapperVC = ScanQRCodeWrapperVC()
|
|
result.delegate = self
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(using dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
setNavBarTitle("communityJoin".localized())
|
|
view.themeBackgroundColor = .backgroundSecondary
|
|
|
|
// Only account for navigation header when view controller
|
|
// presentation type is `fullScreen`
|
|
var navBarHeight: CGFloat {
|
|
switch modalPresentationStyle {
|
|
case .fullScreen: return (navigationController?.navigationBar.frame.size.height ?? 0)
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
|
closeButton.themeTintColor = .textPrimary
|
|
navigationItem.rightBarButtonItem = closeButton
|
|
|
|
// Page VC
|
|
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
|
pages = [ enterURLVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
|
pageVC.dataSource = self
|
|
pageVC.delegate = self
|
|
pageVC.setViewControllers([ enterURLVC ], direction: .forward, animated: false, completion: nil)
|
|
|
|
// Tab bar
|
|
view.addSubview(tabBar)
|
|
tabBar.pin(.leading, to: .leading, of: view)
|
|
tabBar.pin(.top, to: .top, of: view, withInset: navBarHeight)
|
|
tabBar.pin(.trailing, to: .trailing, of: view)
|
|
|
|
// Page VC constraints
|
|
let pageVCView = pageVC.view!
|
|
view.addSubview(pageVCView)
|
|
pageVCView.pin(.leading, to: .leading, of: view)
|
|
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
|
pageVCView.pin(.trailing, to: .trailing, of: view)
|
|
pageVCView.pin(.bottom, to: .bottom, of: view)
|
|
|
|
let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
|
|
let height: CGFloat = ((navigationController?.view.bounds.height ?? 0) - (navigationController?.navigationBar.frame.size.height ?? 0) - TabBar.snHeight - statusBarHeight)
|
|
let size: CGSize = CGSize(width: UIScreen.main.bounds.width, height: height)
|
|
enterURLVC.constrainSize(to: size)
|
|
scanQRCodePlaceholderVC.constrainSize(to: size)
|
|
|
|
navigatableState.setupBindings(viewController: self, disposables: &disposables)
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|
let height: CGFloat = (size.height - TabBar.snHeight)
|
|
let size: CGSize = CGSize(width: size.width, height: height)
|
|
enterURLVC.constrainSize(to: size)
|
|
scanQRCodePlaceholderVC.constrainSize(to: size)
|
|
enterURLVC.suggestionGrid.refreshLayout(with: size.width - 2 * Values.largeSpacing)
|
|
}
|
|
|
|
// 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]
|
|
}
|
|
|
|
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
|
guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil }
|
|
|
|
return pages[index + 1]
|
|
}
|
|
|
|
fileprivate func handleCameraAccessGranted() {
|
|
DispatchQueue.main.async {
|
|
self.pages[1] = self.scanQRCodeWrapperVC
|
|
self.pageVC.setViewControllers([ self.scanQRCodeWrapperVC ], direction: .forward, animated: false, completion: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Updating
|
|
|
|
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
|
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
|
|
|
|
targetVCIndex = index
|
|
}
|
|
|
|
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) {
|
|
guard isCompleted, let index = targetVCIndex else { return }
|
|
|
|
tabBar.selectTab(at: index)
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
@objc private func close() {
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onSuccess: (() -> ())?, onError: (() -> ())?) {
|
|
joinOpenGroup(with: string, onError: onError)
|
|
}
|
|
|
|
fileprivate func joinOpenGroup(with urlString: String, onError: (() -> ())?) {
|
|
// A V2 open group URL will look like: <optional scheme> + <host> + <optional port> + <room> + <public key>
|
|
// The host doesn't parse if no explicit scheme is provided
|
|
guard let (room, server, publicKey) = LibSession.parseCommunity(url: urlString) else {
|
|
showError(
|
|
title: "communityEnterUrlErrorInvalid".localized(),
|
|
message: "communityEnterUrlErrorInvalidDescription".localized(),
|
|
onError: onError
|
|
)
|
|
return
|
|
}
|
|
|
|
joinOpenGroup(roomToken: room, server: server, publicKey: publicKey, shouldOpenCommunity: true, onError: onError)
|
|
}
|
|
|
|
fileprivate func joinOpenGroup(
|
|
roomToken: String,
|
|
server: String,
|
|
publicKey: String,
|
|
shouldOpenCommunity: Bool,
|
|
onError: (() -> ())?
|
|
) {
|
|
Task.detached(priority: .userInitiated) { [weak self, dependencies] in
|
|
let hasExistingOpenGroup: Bool = await dependencies[singleton: .communityManager].hasExistingCommunity(
|
|
roomToken: roomToken,
|
|
server: server,
|
|
publicKey: publicKey
|
|
)
|
|
|
|
guard !hasExistingOpenGroup else {
|
|
await MainActor.run { [weak self] in
|
|
self?.showToast(
|
|
text: "communityJoinedAlready".localized(),
|
|
backgroundColor: .backgroundSecondary
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
await self?.joinOpenGroupAfterExistingCheck(
|
|
roomToken: roomToken,
|
|
server: server,
|
|
publicKey: publicKey,
|
|
shouldOpenCommunity: shouldOpenCommunity,
|
|
onError: onError
|
|
)
|
|
}
|
|
}
|
|
|
|
@MainActor private func joinOpenGroupAfterExistingCheck(
|
|
roomToken: String,
|
|
server: String,
|
|
publicKey: String,
|
|
shouldOpenCommunity: Bool,
|
|
onError: (() -> ())?
|
|
) {
|
|
guard !isJoining, let navigationController: UINavigationController = navigationController else {
|
|
return
|
|
}
|
|
|
|
isJoining = true
|
|
|
|
ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self, dependencies] _ in
|
|
dependencies[singleton: .storage]
|
|
.writePublisher { db in
|
|
dependencies[singleton: .communityManager].add(
|
|
db,
|
|
roomToken: roomToken,
|
|
server: server,
|
|
publicKey: publicKey,
|
|
joinedAt: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000),
|
|
forceVisible: false
|
|
)
|
|
}
|
|
.flatMap { successfullyAddedGroup in
|
|
dependencies[singleton: .communityManager].performInitialRequestsAfterAdd(
|
|
queue: DispatchQueue.global(qos: .userInitiated),
|
|
successfullyAddedGroup: successfullyAddedGroup,
|
|
roomToken: roomToken,
|
|
server: server,
|
|
publicKey: publicKey
|
|
)
|
|
}
|
|
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
|
.receive(on: DispatchQueue.main)
|
|
.sinkUntilComplete(
|
|
receiveCompletion: { result in
|
|
switch result {
|
|
case .failure(let error):
|
|
// If there was a failure then the group will be in invalid state until
|
|
// the next launch so remove it (the user will be left on the previous
|
|
// screen so can re-trigger the join)
|
|
dependencies[singleton: .storage].writeAsync { db in
|
|
try dependencies[singleton: .communityManager].delete(
|
|
db,
|
|
openGroupId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
|
skipLibSessionUpdate: false
|
|
)
|
|
}
|
|
|
|
// Show the user an error indicating they failed to properly join the group
|
|
self?.isJoining = false
|
|
self?.dismiss(animated: true) { // Dismiss the loader
|
|
self?.showError(
|
|
title: "communityJoinError".localized(),
|
|
message: "\(error)",
|
|
onError: onError
|
|
)
|
|
}
|
|
|
|
case .finished:
|
|
guard shouldOpenCommunity else {
|
|
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
|
return
|
|
}
|
|
|
|
Task.detached(priority: .userInitiated) {
|
|
await dependencies[singleton: .app].presentConversationCreatingIfNeeded(
|
|
for: OpenGroup.idFor(roomToken: roomToken, server: server),
|
|
variant: .community,
|
|
action: .none,
|
|
dismissing: self?.presentingViewController,
|
|
animated: false
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
private func showError(title: String, message: String = "", onError: (() -> ())?) {
|
|
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: title,
|
|
body: .text(message),
|
|
cancelTitle: "okay".localized(),
|
|
cancelStyle: .alert_text,
|
|
afterClosed: onError
|
|
)
|
|
)
|
|
self.navigationController?.present(confirmationModal, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - EnterURLVC
|
|
|
|
private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate {
|
|
weak var joinOpenGroupVC: JoinOpenGroupVC?
|
|
|
|
private let dependencies: Dependencies
|
|
private var isKeyboardShowing = false
|
|
private var bottomConstraint: NSLayoutConstraint!
|
|
private let bottomMargin: CGFloat = (UIDevice.current.isIPad ? Values.largeSpacing : 0)
|
|
|
|
// MARK: - UI
|
|
|
|
private var keyboardTransitionSnapshot1: UIView?
|
|
private var keyboardTransitionSnapshot2: UIView?
|
|
|
|
private lazy var urlTextView: SNTextView = {
|
|
let result: SNTextView = SNTextView(placeholder: "communityEnterUrl".localized()) { [weak self] text in
|
|
self?.joinButton.isEnabled = !text.isEmpty
|
|
}
|
|
result.keyboardType = .URL
|
|
result.autocapitalizationType = .none
|
|
result.autocorrectionType = .no
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var joinButton: UIButton = {
|
|
let result: SessionButton = SessionButton(style: .bordered, size: .large)
|
|
result.setTitle("join".localized(), for: UIControl.State.normal)
|
|
result.addTarget(self, action: #selector(joinOpenGroup), for: .touchUpInside)
|
|
result.isEnabled = false
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var suggestionGridTitleLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.setContentHuggingPriority(.required, for: .vertical)
|
|
result.font = .boldSystemFont(ofSize: Values.largeFontSize)
|
|
result.text = "communityJoinOfficial".localized()
|
|
result.themeTextColor = .textPrimary
|
|
result.lineBreakMode = .byWordWrapping
|
|
result.numberOfLines = 0
|
|
|
|
return result
|
|
}()
|
|
|
|
lazy var suggestionGrid: OpenGroupSuggestionGrid = {
|
|
let maxWidth: CGFloat = (UIScreen.main.bounds.width - Values.largeSpacing * 2)
|
|
let result: OpenGroupSuggestionGrid = OpenGroupSuggestionGrid(maxWidth: maxWidth, using: dependencies)
|
|
result.delegate = self
|
|
|
|
return result
|
|
}()
|
|
|
|
private var viewWidth: NSLayoutConstraint?
|
|
private var viewHeight: NSLayoutConstraint?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(using dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
// Remove background color
|
|
view.themeBackgroundColor = .clear
|
|
|
|
// Next button
|
|
let joinButtonContainer = UIView(
|
|
wrapping: joinButton,
|
|
withInsets: UIEdgeInsets(top: 0, leading: 80, bottom: 0, trailing: 80),
|
|
shouldAdaptForIPadWithWidth: Values.iPadButtonWidth
|
|
)
|
|
|
|
// Stack view
|
|
let stackView = UIStackView(
|
|
arrangedSubviews: [
|
|
urlTextView,
|
|
UIView.spacer(withHeight: Values.mediumSpacing),
|
|
suggestionGridTitleLabel,
|
|
UIView.spacer(withHeight: Values.mediumSpacing),
|
|
suggestionGrid,
|
|
UIView.vStretchingSpacer(),
|
|
joinButtonContainer
|
|
]
|
|
)
|
|
stackView.axis = .vertical
|
|
stackView.alignment = .fill
|
|
stackView.layoutMargins = UIEdgeInsets(
|
|
top: Values.largeSpacing,
|
|
leading: Values.largeSpacing,
|
|
bottom: Values.smallSpacing,
|
|
trailing: Values.largeSpacing
|
|
)
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
view.addSubview(stackView)
|
|
|
|
stackView.pin(.leading, to: .leading, of: view)
|
|
stackView.pin(.top, to: .top, of: view)
|
|
view.pin(.trailing, to: .trailing, of: stackView)
|
|
|
|
bottomConstraint = view.pin(.bottom, to: .bottom, of: stackView, withInset: bottomMargin)
|
|
|
|
// Constraints
|
|
viewWidth = view.set(.width, to: UIScreen.main.bounds.width)
|
|
|
|
// Dismiss keyboard on tap
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
|
tapGestureRecognizer.delegate = self
|
|
view.addGestureRecognizer(tapGestureRecognizer)
|
|
|
|
// Listen to keyboard notifications
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleKeyboardWillChangeFrameNotification(_:)),
|
|
name: UIResponder.keyboardWillChangeFrameNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleKeyboardWillHideNotification(_:)),
|
|
name: UIResponder.keyboardWillHideNotification,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
// MARK: - General
|
|
|
|
func constrainSize(to size: CGSize) {
|
|
if viewWidth == nil {
|
|
viewWidth = view.set(.width, to: size.width)
|
|
} else {
|
|
viewWidth?.constant = size.width
|
|
}
|
|
|
|
if viewHeight == nil {
|
|
viewHeight = view.set(.height, to: size.height)
|
|
} else {
|
|
viewHeight?.constant = size.height
|
|
}
|
|
}
|
|
|
|
@objc private func dismissKeyboard() {
|
|
urlTextView.resignFirstResponder()
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
let location = gestureRecognizer.location(in: view)
|
|
|
|
return (
|
|
(!suggestionGrid.isHidden && !suggestionGrid.frame.contains(location)) ||
|
|
(suggestionGrid.isHidden && location.y > urlTextView.frame.maxY)
|
|
)
|
|
}
|
|
|
|
func join(_ room: Network.SOGS.Room) {
|
|
joinOpenGroupVC?.joinOpenGroup(
|
|
roomToken: room.token,
|
|
server: Network.SOGS.defaultServer,
|
|
publicKey: Network.SOGS.defaultServerPublicKey,
|
|
shouldOpenCommunity: true,
|
|
onError: nil
|
|
)
|
|
}
|
|
|
|
@objc private func joinOpenGroup() {
|
|
let url = urlTextView.text?.trimmingCharacters(in: .whitespaces) ?? ""
|
|
joinOpenGroupVC?.joinOpenGroup(with: url, onError: nil)
|
|
}
|
|
|
|
// MARK: - Updating
|
|
|
|
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
|
|
guard !isKeyboardShowing else { return }
|
|
isKeyboardShowing = true
|
|
|
|
guard let endFrame: CGRect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return }
|
|
guard endFrame.minY < UIScreen.main.bounds.height else { return }
|
|
|
|
let duration = max(0.25, ((notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0))
|
|
|
|
// Add snapshots for the suggestion grid
|
|
UIView.performWithoutAnimation {
|
|
self.keyboardTransitionSnapshot1 = self.suggestionGridTitleLabel.snapshotView(afterScreenUpdates: false)
|
|
self.keyboardTransitionSnapshot1?.frame = self.suggestionGridTitleLabel.frame
|
|
self.suggestionGridTitleLabel.alpha = 0
|
|
|
|
if let snapshot1: UIView = self.keyboardTransitionSnapshot1 {
|
|
self.suggestionGridTitleLabel.superview?.addSubview(snapshot1)
|
|
}
|
|
|
|
self.keyboardTransitionSnapshot2 = self.suggestionGrid.snapshotView(afterScreenUpdates: false)
|
|
self.keyboardTransitionSnapshot2?.frame = self.suggestionGrid.frame
|
|
self.suggestionGrid.alpha = 0
|
|
|
|
if let snapshot2: UIView = self.keyboardTransitionSnapshot2 {
|
|
self.suggestionGrid.superview?.addSubview(snapshot2)
|
|
}
|
|
self.view.setNeedsLayout()
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
|
|
UIView.animate(
|
|
withDuration: duration,
|
|
animations: { [weak self] in
|
|
self?.keyboardTransitionSnapshot1?.alpha = 0
|
|
self?.keyboardTransitionSnapshot2?.alpha = 0
|
|
self?.suggestionGridTitleLabel.isHidden = true
|
|
self?.suggestionGrid.isHidden = true
|
|
self?.bottomConstraint.constant = (endFrame.size.height + (self?.bottomMargin ?? 0))
|
|
self?.view.layoutIfNeeded()
|
|
},
|
|
completion: nil
|
|
)
|
|
}
|
|
|
|
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
|
|
guard isKeyboardShowing else { return }
|
|
|
|
let duration = max(0.25, ((notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0))
|
|
isKeyboardShowing = false
|
|
|
|
self.suggestionGrid.alpha = 0
|
|
self.suggestionGridTitleLabel.alpha = 0
|
|
|
|
UIView.animate(
|
|
withDuration: duration,
|
|
animations: { [weak self] in
|
|
self?.keyboardTransitionSnapshot1?.alpha = 1
|
|
self?.keyboardTransitionSnapshot2?.alpha = 1
|
|
self?.suggestionGrid.isHidden = false
|
|
self?.suggestionGridTitleLabel.isHidden = false
|
|
self?.bottomConstraint.constant = (self?.bottomMargin ?? 0)
|
|
self?.view.layoutIfNeeded()
|
|
},
|
|
completion: { [weak self] _ in
|
|
self?.keyboardTransitionSnapshot1?.removeFromSuperview()
|
|
self?.keyboardTransitionSnapshot2?.removeFromSuperview()
|
|
self?.keyboardTransitionSnapshot1 = nil
|
|
self?.keyboardTransitionSnapshot2 = nil
|
|
self?.suggestionGridTitleLabel.alpha = 1
|
|
self?.suggestionGrid.alpha = 1
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private final class ScanQRCodePlaceholderVC: UIViewController {
|
|
private let dependencies: Dependencies
|
|
weak var joinOpenGroupVC: JoinOpenGroupVC?
|
|
|
|
private var viewWidth: NSLayoutConstraint?
|
|
private var viewHeight: NSLayoutConstraint?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(using dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
// Remove background color
|
|
view.themeBackgroundColor = .clear
|
|
|
|
// Explanation label
|
|
let explanationLabel = UILabel()
|
|
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
|
explanationLabel.text = "cameraGrantAccessQr"
|
|
.put(key: "app_name", value: Constants.app_name)
|
|
.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.setTitle("theContinue".localized(), for: .normal)
|
|
callToActionButton.setThemeTitleColor(.primary, for: .normal)
|
|
callToActionButton.addTarget(self, action: #selector(requestCameraAccess), for: .touchUpInside)
|
|
|
|
// Stack view
|
|
let stackView = UIStackView(arrangedSubviews: [ explanationLabel, callToActionButton ])
|
|
stackView.axis = .vertical
|
|
stackView.spacing = Values.mediumSpacing
|
|
stackView.alignment = .center
|
|
|
|
// Constraints
|
|
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
|
|
}
|
|
|
|
func constrainSize(to size: CGSize) {
|
|
if viewWidth == nil {
|
|
viewWidth = view.set(.width, to: size.width)
|
|
} else {
|
|
viewWidth?.constant = size.width
|
|
}
|
|
|
|
if viewHeight == nil {
|
|
viewHeight = view.set(.height, to: size.height)
|
|
} else {
|
|
viewHeight?.constant = size.height
|
|
}
|
|
}
|
|
|
|
@objc private func requestCameraAccess() {
|
|
Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] granted in
|
|
guard granted else { return }
|
|
self?.joinOpenGroupVC?.handleCameraAccessGranted()
|
|
}
|
|
}
|
|
}
|