Add “documents” section in the all media screen for a conversation
This commit is contained in:
parent
34fea96db3
commit
ccc46dfbf8
|
@ -232,4 +232,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: 6ab902a81a379cc2c0a9a92c334c78d413190338
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
|
||||
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
|
||||
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
|
||||
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
|
||||
7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1163,6 +1166,7 @@
|
|||
7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
|
||||
7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
|
||||
7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)))"
|
||||
}
|
||||
}
|
|
@ -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<Interaction> = TypedTableAlias()
|
||||
let attachment: TypedTableAlias<Attachment> = 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 }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue