From ccc46dfbf8c44c1d62616a2a55f6ae8514d78000 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Thu, 7 Jul 2022 16:40:13 +1000 Subject: [PATCH] =?UTF-8?q?Add=20=E2=80=9Cdocuments=E2=80=9D=20section=20i?= =?UTF-8?q?n=20the=20all=20media=20screen=20for=20a=20conversation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Podfile.lock | 2 +- Session.xcodeproj/project.pbxproj | 8 + .../AllMediaViewController.swift | 100 +++++ .../DocumentTitleViewController.swift | 375 ++++++++++++++++++ .../MediaGalleryViewModel.swift | 67 +++- .../MediaPageViewController.swift | 2 +- .../Translations/en.lproj/Localizable.strings | 3 +- .../Utilities/CommonStrings.swift | 4 + 8 files changed, 552 insertions(+), 9 deletions(-) create mode 100644 Session/Media Viewing & Editing/AllMediaViewController.swift create mode 100644 Session/Media Viewing & Editing/DocumentTitleViewController.swift diff --git a/Podfile.lock b/Podfile.lock index 70045a1da..bab1ae426 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -232,4 +232,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 6ab902a81a379cc2c0a9a92c334c78d413190338 -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 7c1a8f752..e422c0866 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -124,6 +124,7 @@ 7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; }; 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; }; + 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; @@ -146,6 +147,7 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; }; 7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */; }; 7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; }; + 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; }; @@ -1141,6 +1143,7 @@ 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; + 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; @@ -1163,6 +1166,7 @@ 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2855,6 +2859,7 @@ FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */, 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */, 454A84032059C787008B8C75 /* MediaTileViewController.swift */, + 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */, 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */, 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */, @@ -2865,6 +2870,7 @@ 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */, 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */, 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, + 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */, ); path = "Media Viewing & Editing"; sourceTree = ""; @@ -5298,6 +5304,7 @@ FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */, 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, + 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */, C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */, B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */, C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */, @@ -5308,6 +5315,7 @@ B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */, + 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, diff --git a/Session/Media Viewing & Editing/AllMediaViewController.swift b/Session/Media Viewing & Editing/AllMediaViewController.swift new file mode 100644 index 000000000..121db8b43 --- /dev/null +++ b/Session/Media Viewing & Editing/AllMediaViewController.swift @@ -0,0 +1,100 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import QuartzCore +import GRDB +import DifferenceKit +import SessionUIKit +import SignalUtilitiesKit + +public class AllMediaViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + private var pages: [UIViewController] = [] + private var targetVCIndex: Int? + + // MARK: Components + private lazy var tabBar: TabBar = { + let tabs = [ + TabBar.Tab(title: MediaStrings.media) { [weak self] in + guard let self = self else { return } + self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil) + }, + TabBar.Tab(title: MediaStrings.document) { [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 var mediaTitleViewController: MediaTileViewController + private var documentTitleViewController: DocumentTileViewController + + init(mediaTitleViewController: MediaTileViewController, documentTitleViewController: DocumentTileViewController) { + self.mediaTitleViewController = mediaTitleViewController + self.documentTitleViewController = documentTitleViewController + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + notImplemented() + } + + // MARK: Lifecycle + public override func viewDidLoad() { + super.viewDidLoad() + + // Add a custom back button if this is the only view controller + if self.navigationController?.viewControllers.first == self { + let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton)) + self.navigationItem.leftBarButtonItem = backButton + } + + ViewControllerUtilities.setUpDefaultSessionStyle( + for: self, + title: MediaStrings.allMedia, + hasCustomBackButton: false + ) + + // Set up page VC + pages = [ mediaTitleViewController, documentTitleViewController ] + pageVC.dataSource = self + pageVC.delegate = self + pageVC.setViewControllers([ mediaTitleViewController ], direction: .forward, animated: false, completion: nil) + // Set up tab bar + view.addSubview(tabBar) + tabBar.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: view) + // Set up page VC constraints + let pageVCView = pageVC.view! + view.addSubview(pageVCView) + pageVCView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) + pageVCView.pin(.top, to: .bottom, of: tabBar) + } + + // MARK: General + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil } + return pages[index - 1] + } + + public 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] + } + + // MARK: Updating + public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return } + targetVCIndex = index + } + + public 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 public func didPressDismissButton() { + dismiss(animated: true, completion: nil) + } +} diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift new file mode 100644 index 000000000..820e2eabb --- /dev/null +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -0,0 +1,375 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import QuartzCore +import GRDB +import DifferenceKit +import SessionUIKit +import SignalUtilitiesKit + +public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + + /// This should be larger than one screen size so we don't have to call it multiple times in rapid succession, but not + /// so large that loading get's really chopping + static let itemPageSize: Int = Int(11 * itemsPerPortraitRow) + static let itemsPerPortraitRow: CGFloat = 4 + static let interItemSpacing: CGFloat = 2 + static let footerBarHeight: CGFloat = 40 + static let loadMoreHeaderHeight: CGFloat = 100 + + private let viewModel: MediaGalleryViewModel + private var hasLoadedInitialData: Bool = false + private var didFinishInitialLayout: Bool = false + private var isAutoLoadingNextPage: Bool = false + private var currentTargetOffset: CGPoint? + + // MARK: - Initialization + + init(viewModel: MediaGalleryViewModel) { + self.viewModel = viewModel + Storage.shared.addObserver(viewModel.pagedDataObserver) + + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - UI + + override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .allButUpsideDown + } + + lazy var tableView: UITableView = { + let result = UITableView(frame: .zero, style: .grouped) + result.backgroundColor = Colors.navigationBarBackground + result.separatorStyle = .none + result.showsVerticalScrollIndicator = false + result.register(view: DocumentCell.self) + result.delegate = self + result.dataSource = self + // Feels a bit weird to have content smashed all the way to the bottom edge. + result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) + + return result + }() + + // MARK: - Lifecycle + + override public func viewDidLoad() { + super.viewDidLoad() + + // Add a custom back button if this is the only view controller + if self.navigationController?.viewControllers.first == self { + let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton)) + self.navigationItem.leftBarButtonItem = backButton + } + + ViewControllerUtilities.setUpDefaultSessionStyle( + for: self, + title: MediaStrings.document, + hasCustomBackButton: false + ) + + view.addSubview(self.tableView) + tableView.autoPin(toEdgesOf: view) + + // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidResignActive(_:)), + name: UIApplication.didEnterBackgroundNotification, object: nil + ) + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + startObservingChanges() + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.didFinishInitialLayout = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + stopObservingChanges() + } + + @objc func applicationDidBecomeActive(_ notification: Notification) { + startObservingChanges() + } + + @objc func applicationDidResignActive(_ notification: Notification) { + stopObservingChanges() + } + + // MARK: - Updating + + private func performInitialScrollIfNeeded() { + // Ensure this hasn't run before and that we have data (The 'galleryData' will always + // contain something as the 'empty' state is a section within 'galleryData') + guard !self.didFinishInitialLayout && self.hasLoadedInitialData else { return } + + // If we have a focused item then we want to scroll to it + guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return } + + Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)") + self.view.layoutIfNeeded() + self.tableView.scrollToRow(at: focusedIndexPath, at: .middle, animated: false) + + // Now that the data has loaded we need to check if either of the "load more" sections are + // visible and trigger them if so + // + // Note: We do it this way as we want to trigger the load behaviour for the first section + // if it has one before trying to trigger the load behaviour for the last section + self.autoLoadNextPageIfNeeded() + } + + private func autoLoadNextPageIfNeeded() { + guard !self.isAutoLoadingNextPage else { return } + + self.isAutoLoadingNextPage = true + + DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in + self?.isAutoLoadingNextPage = false + + // Note: We sort the headers as we want to prioritise loading newer pages over older ones + let sortedVisibleIndexPaths: [IndexPath] = (self?.tableView.indexPathsForVisibleRows ?? []).sorted() + + for headerIndexPath in sortedVisibleIndexPaths { + let section: MediaGalleryViewModel.SectionModel? = self?.viewModel.galleryData[safe: headerIndexPath.section] + + switch section?.model { + case .loadNewer, .loadOlder: + // Attachments are loaded in descending order so 'loadOlder' actually corresponds with + // 'pageAfter' in this case + self?.viewModel.pagedDataObserver?.load(section?.model == .loadOlder ? + .pageAfter : + .pageBefore + ) + return + + default: continue + } + } + } + } + + private func startObservingChanges() { + // Start observing for data changes (will callback on the main thread) + self.viewModel.onGalleryChange = { [weak self] updatedGalleryData in + self?.handleUpdates(updatedGalleryData) + } + } + + private func stopObservingChanges() { + // Note: The 'pagedDataObserver' will continue to get changes but + // we don't want to trigger any UI updates + self.viewModel.onGalleryChange = nil + } + + private func handleUpdates(_ updatedGalleryData: [MediaGalleryViewModel.SectionModel]) { + // Ensure the first load runs without animations (if we don't do this the cells will animate + // in from a frame of CGRect.zero) + guard hasLoadedInitialData else { + self.hasLoadedInitialData = true + self.viewModel.updateGalleryData(updatedGalleryData) + + UIView.performWithoutAnimation { + self.tableView.reloadData() + self.performInitialScrollIfNeeded() + } + return + } + + + } + + // MARK: - Interactions + + @objc public func didPressDismissButton() { + let presentedNavController: UINavigationController? = (self.presentingViewController as? UINavigationController) + let mediaPageViewController: MediaPageViewController? = ( + (presentedNavController?.viewControllers.last as? MediaPageViewController) ?? + (self.presentingViewController as? MediaPageViewController) + ) + + // If the album was presented from a 'MediaPageViewController' and it has no more data (ie. + // all album items had been deleted) then dismiss to the screen before that one + guard mediaPageViewController?.viewModel.albumData.isEmpty != true else { + presentedNavController?.presentingViewController?.dismiss(animated: true, completion: nil) + return + } + + dismiss(animated: true, completion: nil) + } + + // MARK: - UITableViewDataSource + + public func numberOfSections(in tableView: UITableView) -> Int { + print("numberOfSections: \(self.viewModel.galleryData.count)") + return self.viewModel.galleryData.count + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + print("rows: \(self.viewModel.galleryData[section])") + return self.viewModel.galleryData[section].elements.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: DocumentCell = tableView.dequeue(type: DocumentCell.self, for: indexPath) + cell.update(with: self.viewModel.galleryData[indexPath.section].elements[indexPath.row]) + return cell + } + + // MARK: - UITableViewDelegate + + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section] + + switch section.model { + case .emptyGallery, .loadOlder, .loadNewer: + return nil + + case .galleryMonth(let date): + let headerView: UIView = UIView() + let label = UILabel() + label.textColor = Colors.text + label.text = date.localizedString + + let blurEffect = UIBlurEffect(style: .dark) + let blurEffectView = UIVisualEffectView(effect: blurEffect) + + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + headerView.backgroundColor = isLightMode ? Colors.cellBackground : UIColor.ows_black.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor) + + headerView.addSubview(blurEffectView) + headerView.addSubview(label) + + blurEffectView.autoPinEdgesToSuperviewEdges() + blurEffectView.isHidden = isLightMode + label.autoPinEdge(toSuperviewMargin: .trailing) + label.autoPinEdge(toSuperviewMargin: .leading) + label.autoVCenterInSuperview() + + return headerView + } + } + + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 50 + } +} + +class DocumentCell: UITableViewCell { + + // MARK: - Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setUpViewHierarchy() + setupLayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + setUpViewHierarchy() + setupLayout() + } + + // MARK: - UI + + private static let iconImageViewSize: CGSize = CGSize(width: 31, height: 40) + + private let iconImageView: UIImageView = { + let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "File").withRenderingMode(.alwaysTemplate)) + result.translatesAutoresizingMaskIntoConstraints = false + result.tintColor = Colors.text + + return result + }() + + private let titleLabel: UILabel = { + let result: UILabel = UILabel() + result.translatesAutoresizingMaskIntoConstraints = false + result.setContentHuggingPriority(.defaultHigh, for: .horizontal) + result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.textColor = Colors.text + result.lineBreakMode = .byTruncatingTail + + return result + }() + + private let detailLabel: UILabel = { + let result: UILabel = UILabel() + result.translatesAutoresizingMaskIntoConstraints = false + result.setContentHuggingPriority(.defaultHigh, for: .horizontal) + result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + result.font = .systemFont(ofSize: Values.smallFontSize) + result.textColor = Colors.text + result.lineBreakMode = .byTruncatingTail + + return result + }() + + private func setUpViewHierarchy() { + backgroundColor = Colors.cellBackground + selectedBackgroundView = UIView() + selectedBackgroundView?.backgroundColor = Colors.cellSelected + + + contentView.addSubview(iconImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(detailLabel) + } + + // MARK: - Layout + + private func setupLayout() { + NSLayoutConstraint.activate([ + contentView.heightAnchor.constraint(equalToConstant: 68), + + iconImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: Values.mediumSpacing), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: Self.iconImageViewSize.width), + iconImageView.heightAnchor.constraint(equalToConstant: Self.iconImageViewSize.height), + + titleLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing), + titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing), + titleLabel.topAnchor.constraint(equalTo: iconImageView.topAnchor), + + detailLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing), + detailLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing), + detailLabel.bottomAnchor.constraint(equalTo: iconImageView.bottomAnchor), + ]) + } + + // MARK: - Content + + func update(with item: MediaGalleryViewModel.Item) { + let attachment = item.attachment + titleLabel.text = attachment.sourceFilename ?? "File" + detailLabel.text = "\(OWSFormat.formatFileSize(UInt(attachment.byteCount)))" + } +} diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index cc425d2d3..21dc80624 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -18,12 +18,19 @@ public class MediaGalleryViewModel { case loadNewer } + // MARK: Media type + public enum MediaType { + case media + case document + } + // MARK: - Variables public let threadId: String public let threadVariant: SessionThread.Variant private var focusedAttachmentId: String? public private(set) var focusedIndexPath: IndexPath? + public var mediaType: MediaType /// This value is the current state of an album view private var cachedInteractionIdBefore: Atomic<[Int64: Int64]> = Atomic([:]) @@ -54,6 +61,7 @@ public class MediaGalleryViewModel { threadId: String, threadVariant: SessionThread.Variant, isPagedData: Bool, + mediaType: MediaType, pageSize: Int = 1, focusedAttachmentId: String? = nil, performInitialQuerySync: Bool = false @@ -62,6 +70,7 @@ public class MediaGalleryViewModel { self.threadVariant = threadVariant self.focusedAttachmentId = focusedAttachmentId self.pagedDataObserver = nil + self.mediaType = mediaType guard isPagedData else { return } @@ -80,7 +89,7 @@ public class MediaGalleryViewModel { ) ], joinSQL: Item.joinSQL, - filterSQL: Item.filterSQL(threadId: threadId), + filterSQL: Item.filterSQL(threadId: threadId, mediaType: self.mediaType), orderSQL: Item.galleryOrderSQL, dataQuery: Item.baseQuery(orderSQL: Item.galleryOrderSQL), onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in @@ -243,12 +252,12 @@ public class MediaGalleryViewModel { """ }() - fileprivate static func filterSQL(threadId: String) -> SQL { + fileprivate static func filterSQL(threadId: String, mediaType: MediaType) -> SQL { let interaction: TypedTableAlias = TypedTableAlias() let attachment: TypedTableAlias = TypedTableAlias() return SQL(""" - \(attachment[.isVisualMedia]) = true AND + \(attachment[.isVisualMedia]) = \(mediaType == .media ? true : false) AND \(attachment[.isValid]) = true AND \(interaction[.threadId]) = \(threadId) """) @@ -503,7 +512,8 @@ public class MediaGalleryViewModel { let viewModel: MediaGalleryViewModel = MediaGalleryViewModel( threadId: threadId, threadVariant: threadVariant, - isPagedData: false + isPagedData: false, + mediaType: .media ) viewModel.loadAndCacheAlbumData(for: interactionId) viewModel.replaceAlbumObservation(toObservationFor: interactionId) @@ -528,7 +538,7 @@ public class MediaGalleryViewModel { return navController } - public static func createTileViewController( + public static func createMediaTileViewController( threadId: String, threadVariant: SessionThread.Variant, focusedAttachmentId: String?, @@ -538,6 +548,7 @@ public class MediaGalleryViewModel { threadId: threadId, threadVariant: threadVariant, isPagedData: true, + mediaType: .media, pageSize: MediaTileViewController.itemPageSize, focusedAttachmentId: focusedAttachmentId, performInitialQuerySync: performInitialQuerySync @@ -547,6 +558,50 @@ public class MediaGalleryViewModel { viewModel: viewModel ) } + + public static func createDocumentTitleViewController( + threadId: String, + threadVariant: SessionThread.Variant, + focusedAttachmentId: String?, + performInitialQuerySync: Bool = false + ) -> DocumentTileViewController { + let viewModel: MediaGalleryViewModel = MediaGalleryViewModel( + threadId: threadId, + threadVariant: threadVariant, + isPagedData: true, + mediaType: .document, + pageSize: MediaTileViewController.itemPageSize, + focusedAttachmentId: focusedAttachmentId, + performInitialQuerySync: performInitialQuerySync + ) + + return DocumentTileViewController( + viewModel: viewModel + ) + } + + public static func createAllMediaViewController( + threadId: String, + threadVariant: SessionThread.Variant, + focusedAttachmentId: String?, + performInitialQuerySync: Bool = false + ) -> AllMediaViewController { + let mediaTitleViewController = createMediaTileViewController( + threadId: threadId, + threadVariant: threadVariant, + focusedAttachmentId: focusedAttachmentId, + performInitialQuerySync: performInitialQuerySync) + + let documentTitleViewController = createDocumentTitleViewController( + threadId: threadId, + threadVariant: threadVariant, + focusedAttachmentId: focusedAttachmentId, + performInitialQuerySync: performInitialQuerySync) + + return AllMediaViewController( + mediaTitleViewController: mediaTitleViewController, + documentTitleViewController: documentTitleViewController) + } } // MARK: - Objective-C Support @@ -558,7 +613,7 @@ public class SNMediaGallery: NSObject { @objc(pushTileViewWithSliderEnabledForThreadId:isClosedGroup:isOpenGroup:fromNavController:) static func pushTileView(threadId: String, isClosedGroup: Bool, isOpenGroup: Bool, fromNavController: OWSNavigationController) { fromNavController.pushViewController( - MediaGalleryViewModel.createTileViewController( + MediaGalleryViewModel.createAllMediaViewController( threadId: threadId, threadVariant: { if isClosedGroup { return .closedGroup } diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 7b9f96349..53ecb371b 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -471,7 +471,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // Otherwise if we came via the conversation screen we need to push a new // instance of MediaTileViewController - let tileViewController: MediaTileViewController = MediaGalleryViewModel.createTileViewController( + let tileViewController: MediaTileViewController = MediaGalleryViewModel.createMediaTileViewController( threadId: self.viewModel.threadId, threadVariant: self.viewModel.threadVariant, focusedAttachmentId: currentItem.attachment.id, diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 57ef01ba2..499183d79 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -660,4 +660,5 @@ "MESSAGE_TRIMMING_TITLE" = "Message Trimming"; "MESSAGE_TRIMMING_OPEN_GROUP_TITLE" = "Delete Old Open Group Messages"; "MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION" = "Automatically delete open group messages which are older than 6 months when starting the app"; - +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Document"; diff --git a/SignalUtilitiesKit/Utilities/CommonStrings.swift b/SignalUtilitiesKit/Utilities/CommonStrings.swift index 0bcac0c4f..37dad1418 100644 --- a/SignalUtilitiesKit/Utilities/CommonStrings.swift +++ b/SignalUtilitiesKit/Utilities/CommonStrings.swift @@ -86,6 +86,10 @@ public class NotificationStrings: NSObject { @objc public class MediaStrings: NSObject { @objc static public let allMedia = NSLocalizedString("MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON", comment: "nav bar button item") + @objc + static public let media = NSLocalizedString("MEDIA_TAB_TITLE", comment: "media tab title") + @objc + static public let document = NSLocalizedString("DOCUMENT_TAB_TITLE", comment: "document tab title") } @objc public class SafetyNumberStrings: NSObject {