Merge pull request #646 from RyanRory/add-documents-section

Add documents section for all media page
pull/690/head
RyanZhao 2 years ago committed by GitHub
commit d0e32a8272
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -127,6 +127,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 */; };
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; };
@ -164,6 +165,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 */; };
7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.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, ); }; };
@ -1169,6 +1171,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>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
@ -1206,6 +1209,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>"; };
7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.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>"; };
@ -2945,6 +2949,7 @@
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */,
45F32C1D205718B000A300D5 /* MediaPageViewController.swift */,
454A84032059C787008B8C75 /* MediaTileViewController.swift */,
7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */,
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */,
34969559219B605E00DCFE74 /* ImagePickerController.swift */,
@ -2955,6 +2960,7 @@
4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */,
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */,
);
path = "Media Viewing & Editing";
sourceTree = "<group>";
@ -5434,6 +5440,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 */,
@ -5445,6 +5452,7 @@
7B7037432834B81F000DCF35 /* ReactionContainerView.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,217 @@
// 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)
self.updateSelectButton(updatedData: self.mediaTitleViewController.viewModel.galleryData, inBatchSelectMode: self.mediaTitleViewController.isInBatchSelectMode)
},
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)
self.endSelectMode()
self.navigationItem.rightBarButtonItem = 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)
self.mediaTitleViewController.delegate = self
self.documentTitleViewController.delegate = self
addChild(self.mediaTitleViewController)
addChild(self.documentTitleViewController)
}
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)
addChild(pageVC)
// 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)
}
// MARK: Batch Selection
@objc func didTapSelect(_ sender: Any) {
self.mediaTitleViewController.didTapSelect(sender)
// Don't allow the user to leave mid-selection, so they realized they have
// to cancel (lose) their selection if they leave.
self.navigationItem.hidesBackButton = true
}
@objc func didCancelSelect(_ sender: Any) {
endSelectMode()
}
func endSelectMode() {
self.mediaTitleViewController.endSelectMode()
self.navigationItem.hidesBackButton = false
}
}
// MARK: - UIDocumentInteractionControllerDelegate
extension AllMediaViewController: UIDocumentInteractionControllerDelegate {
public func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
}
// MARK: - DocumentTitleViewControllerDelegate
extension AllMediaViewController: DocumentTileViewControllerDelegate {
public func share(fileUrl: URL) {
let shareVC = UIActivityViewController(activityItems: [ fileUrl ], applicationActivities: nil)
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
navigationController?.present(shareVC, animated: true, completion: nil)
}
public func preview(fileUrl: URL) {
let interactionController: UIDocumentInteractionController = UIDocumentInteractionController(url: fileUrl)
interactionController.delegate = self
interactionController.presentPreview(animated: true)
}
}
// MARK: - DocumentTitleViewControllerDelegate
extension AllMediaViewController: MediaTileViewControllerDelegate {
public func presentdetailViewController(_ detailViewController: UIViewController, animated: Bool) {
self.present(detailViewController, animated: animated)
}
public func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) {
guard !updatedData.isEmpty else {
self.navigationItem.rightBarButtonItem = nil
return
}
if inBatchSelectMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(didCancelSelect)
)
}
else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "BUTTON_SELECT".localized(),
style: .plain,
target: self,
action: #selector(didTapSelect)
)
}
}
}
// MARK: - UIViewControllerTransitioningDelegate
extension AllMediaViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self.mediaTitleViewController.animationController(forPresented: presented, presenting: presenting, source: source)
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self.mediaTitleViewController.animationController(forDismissed: dismissed)
}
}
// MARK: - MediaPresentationContextProvider
extension AllMediaViewController: MediaPresentationContextProvider {
func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {
return self.mediaTitleViewController.mediaPresentationContext(mediaItem: mediaItem, in: coordinateSpace)
}
func snapshotOverlayView(in coordinateSpace: UICoordinateSpace) -> (UIView, CGRect)? {
return self.mediaTitleViewController.snapshotOverlayView(in: coordinateSpace)
}
}

@ -0,0 +1,503 @@
// 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?
public var delegate: DocumentTileViewControllerDelegate?
// 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
}
let isInsertingAtTop: Bool = {
let oldFirstSectionIsLoadMore: Bool = (
self.viewModel.galleryData.first?.model == .loadNewer ||
self.viewModel.galleryData.first?.model == .loadOlder
)
let oldTargetSectionIndex: Int = (oldFirstSectionIsLoadMore ? 1 : 0)
guard
let newTargetSectionIndex = updatedGalleryData
.firstIndex(where: { $0.model == self.viewModel.galleryData[safe: oldTargetSectionIndex]?.model }),
let oldFirstItem: MediaGalleryViewModel.Item = self.viewModel.galleryData[safe: oldTargetSectionIndex]?.elements.first,
let newFirstItemIndex = updatedGalleryData[safe: newTargetSectionIndex]?.elements.firstIndex(of: oldFirstItem)
else { return false }
return (newTargetSectionIndex > oldTargetSectionIndex || newFirstItemIndex > 0)
}()
CATransaction.begin()
if isInsertingAtTop { CATransaction.setDisableActions(true) }
self.tableView.reload(
using: StagedChangeset(source: self.viewModel.galleryData, target: updatedGalleryData),
with: .automatic,
interrupt: { $0.changeCount > MediaTileViewController.itemPageSize }
) { [weak self] updatedData in
self?.viewModel.updateGalleryData(updatedData)
}
CATransaction.setCompletionBlock { [weak self] in
// If one of the "load more" sections is still visible once the animation completes then
// trigger another "load more" (after a small delay to minimize animation bugginess)
self?.autoLoadNextPageIfNeeded()
}
CATransaction.commit()
}
// 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 {
return self.viewModel.galleryData.count
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
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:
let headerView: DocumentStaticHeaderView = DocumentStaticHeaderView()
headerView.configure(
title: {
switch section.model {
case .emptyGallery: return "DOCUMENT_TILES_EMPTY_DOCUMENT".localized()
case .loadOlder: return "DOCUMENT_TILES_LOADING_OLDER_LABEL".localized()
case .loadNewer: return "DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL".localized()
case .galleryMonth: return "" // Impossible case
}
}()
)
return headerView
case .galleryMonth(let date):
let headerView: DocumentSectionHeaderView = DocumentSectionHeaderView()
headerView.configure(title: date.localizedString)
return headerView
}
}
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section]
switch section.model {
case .emptyGallery, .loadOlder, .loadNewer:
return MediaTileViewController.loadMoreHeaderHeight
case .galleryMonth:
return 50
}
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
let attachment: Attachment = self.viewModel.galleryData[indexPath.section].elements[indexPath.row].attachment
guard let originalFilePath: String = attachment.originalFilePath else { return }
let fileUrl: URL = URL(fileURLWithPath: originalFilePath)
// Open a preview of the document for text, pdf or microsoft files
if
attachment.isText ||
attachment.isMicrosoftDoc ||
attachment.contentType == OWSMimeTypeApplicationPdf
{
delegate?.preview(fileUrl: fileUrl)
return
}
// Otherwise share the file
delegate?.share(fileUrl: fileUrl)
}
}
// MARK: - View
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)))"
}
}
class DocumentSectionHeaderView: UIView {
let label: UILabel
override init(frame: CGRect) {
label = UILabel()
label.textColor = Colors.text
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
super.init(frame: frame)
self.backgroundColor = isLightMode ? Colors.cellBackground : UIColor.ows_black.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor)
self.addSubview(blurEffectView)
self.addSubview(label)
blurEffectView.autoPinEdgesToSuperviewEdges()
blurEffectView.isHidden = isLightMode
label.autoPinEdge(toSuperviewMargin: .trailing)
label.autoPinEdge(toSuperviewMargin: .leading)
label.autoVCenterInSuperview()
}
@available(*, unavailable, message: "Unimplemented")
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
public func configure(title: String) {
self.label.text = title
}
}
class DocumentStaticHeaderView: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
label.textColor = Colors.text
label.textAlignment = .center
label.numberOfLines = 0
label.autoPinEdgesToSuperviewMargins(with: UIEdgeInsets(top: 0, leading: Values.largeSpacing, bottom: 0, trailing: Values.largeSpacing))
}
@available(*, unavailable, message: "Unimplemented")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
public func configure(title: String) {
self.label.text = title
}
}
// MARK: - DocumentTitleViewControllerDelegate
public protocol DocumentTileViewControllerDelegate: AnyObject {
func share(fileUrl: URL)
func preview(fileUrl: URL)
}

@ -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,15 +252,27 @@ 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[.isValid]) = true AND
\(interaction[.threadId]) = \(threadId)
""")
switch (mediaType) {
case .media:
return SQL("""
\(attachment[.isVisualMedia]) = true AND
\(attachment[.isValid]) = true AND
\(interaction[.threadId]) = \(threadId)
""")
case .document:
// FIXME: Remove "\(attachment[.sourceFilename]) <> 'session-audio-message'" when all platforms send the voice message properly
return SQL("""
\(attachment[.isVisualMedia]) = false AND
\(attachment[.isValid]) = true AND
\(interaction[.threadId]) = \(threadId) AND
\(attachment[.variant]) = \(Attachment.Variant.standard) AND
\(attachment[.sourceFilename]) <> 'session-audio-message'
""")
}
}
fileprivate static let galleryOrderSQL: SQL = {
@ -509,7 +530,8 @@ public class MediaGalleryViewModel {
let viewModel: MediaGalleryViewModel = MediaGalleryViewModel(
threadId: threadId,
threadVariant: threadVariant,
isPagedData: false
isPagedData: false,
mediaType: .media
)
viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId)
viewModel.replaceAlbumObservation(toObservationFor: interactionId)
@ -534,7 +556,7 @@ public class MediaGalleryViewModel {
return navController
}
public static func createTileViewController(
public static func createMediaTileViewController(
threadId: String,
threadVariant: SessionThread.Variant,
focusedAttachmentId: String?,
@ -544,6 +566,7 @@ public class MediaGalleryViewModel {
threadId: threadId,
threadVariant: threadVariant,
isPagedData: true,
mediaType: .media,
pageSize: MediaTileViewController.itemPageSize,
focusedAttachmentId: focusedAttachmentId,
performInitialQuerySync: performInitialQuerySync
@ -553,6 +576,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
@ -564,7 +631,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 }

@ -458,7 +458,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
// MediaTileViewController then just pop/dismiss the screen
guard
let presentingNavController: UINavigationController = (self.presentingViewController as? UINavigationController),
!(presentingNavController.viewControllers.last is MediaTileViewController)
!(presentingNavController.viewControllers.last is AllMediaViewController)
else {
guard self.navigationController?.viewControllers.count == 1 else {
self.navigationController?.popViewController(animated: true)
@ -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 allMediaViewController: AllMediaViewController = MediaGalleryViewModel.createAllMediaViewController(
threadId: self.viewModel.threadId,
threadVariant: self.viewModel.threadVariant,
focusedAttachmentId: currentItem.attachment.id,
@ -479,9 +479,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
)
let navController: MediaGalleryNavigationController = MediaGalleryNavigationController()
navController.viewControllers = [tileViewController]
navController.viewControllers = [allMediaViewController]
navController.modalPresentationStyle = .overFullScreen
navController.transitioningDelegate = tileViewController
navController.transitioningDelegate = allMediaViewController
self.navigationController?.present(navController, animated: true)
}

@ -17,12 +17,14 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
static let footerBarHeight: CGFloat = 40
static let loadMoreHeaderHeight: CGFloat = 100
private let viewModel: MediaGalleryViewModel
public let viewModel: MediaGalleryViewModel
private var hasLoadedInitialData: Bool = false
private var didFinishInitialLayout: Bool = false
private var isAutoLoadingNextPage: Bool = false
private var currentTargetOffset: CGPoint?
public var delegate: MediaTileViewControllerDelegate?
var isInBatchSelectMode = false {
didSet {
collectionView.allowsMultipleSelection = isInBatchSelectMode
@ -199,8 +201,35 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return }
Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)")
// Note: For some reason 'scrollToItem' doesn't always work properly so we need to manually
// calculate what the offset should be to do the initial scroll
self.view.layoutIfNeeded()
self.collectionView.scrollToItem(at: focusedIndexPath, at: .centeredVertically, animated: false)
let availableHeight: CGFloat = {
// Note: This height will be set before we have properly performed a layout and fitted
// this screen within it's parent UIPagedViewController so we need to try to calculate
// the "actual" height of the collection view
var finalHeight: CGFloat = self.collectionView.frame.height
if let navController: UINavigationController = self.parent?.navigationController {
finalHeight -= navController.navigationBar.frame.height
finalHeight -= (UIApplication.shared.keyWindow?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0)
}
if let tabBar: TabBar = self.parent?.parent?.view.subviews.first as? TabBar {
finalHeight -= tabBar.frame.height
}
return finalHeight
}()
let focusedRect: CGRect = (self.collectionView.layoutAttributesForItem(at: focusedIndexPath)?.frame)
.defaulting(to: .zero)
self.collectionView.contentOffset = CGPoint(
x: 0,
y: (focusedRect.origin.y - (availableHeight / 2) + (focusedRect.height / 2))
)
self.collectionView.collectionViewLayout.invalidateLayout()
// Now that the data has loaded we need to check if either of the "load more" sections are
// visible and trigger them if so
@ -269,6 +298,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
guard hasLoadedInitialData else {
self.hasLoadedInitialData = true
self.viewModel.updateGalleryData(updatedGalleryData)
self.updateSelectButton(updatedData: updatedGalleryData, inBatchSelectMode: isInBatchSelectMode)
UIView.performWithoutAnimation {
self.collectionView.reloadData()
@ -492,12 +522,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
// [ConversationSettingsView]
// [ConversationView]
//
guard
let viewControllers: [UIViewController] = self.navigationController?.viewControllers,
viewControllers.count > 1,
viewControllers[viewControllers.count - 2] is OWSConversationSettingsViewController
else { return }
let detailViewController: UIViewController? = MediaGalleryViewModel.createDetailViewController(
for: self.viewModel.threadId,
threadVariant: self.viewModel.threadVariant,
@ -508,7 +532,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
guard let detailViewController: UIViewController = detailViewController else { return }
self.present(detailViewController, animated: true)
delegate?.presentdetailViewController(detailViewController, animated: true)
return
}
@ -590,26 +614,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
}
func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) {
guard !updatedData.isEmpty else {
self.navigationItem.rightBarButtonItem = nil
return
}
if inBatchSelectMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(didCancelSelect)
)
}
else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "BUTTON_SELECT".localized(),
style: .plain,
target: self,
action: #selector(didTapSelect)
)
}
delegate?.updateSelectButton(updatedData: updatedData, inBatchSelectMode: inBatchSelectMode)
}
@objc func didTapSelect(_ sender: Any) {
@ -624,13 +629,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
// Ensure toolbar doesn't cover bottom row.
self?.collectionView.contentInset.bottom += MediaTileViewController.footerBarHeight
}, completion: nil)
// disabled until at least one item is selected
self.deleteButton.isEnabled = false
// Don't allow the user to leave mid-selection, so they realized they have
// to cancel (lose) their selection if they leave.
self.navigationItem.hidesBackButton = true
}
@objc func didCancelSelect(_ sender: Any) {
@ -650,8 +648,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
self?.collectionView.contentInset.bottom -= MediaTileViewController.footerBarHeight
}, completion: nil)
self.navigationItem.hidesBackButton = false
// Deselect any selected
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)}
}
@ -863,7 +859,12 @@ class GalleryGridCellItem: PhotoGridItem {
extension MediaTileViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard self == presented || self.navigationController == presented else { return nil }
guard
self == presented ||
self.navigationController == presented ||
self.parent == presented ||
self.parent?.navigationController == presented
else { return nil }
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil }
return MediaDismissAnimationController(
@ -872,7 +873,12 @@ extension MediaTileViewController: UIViewControllerTransitioningDelegate {
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard self == dismissed || self.navigationController == dismissed else { return nil }
guard
self == dismissed ||
self.navigationController == dismissed ||
self.parent == dismissed ||
self.parent?.navigationController == dismissed
else { return nil }
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil }
return MediaZoomAnimationController(
@ -923,3 +929,10 @@ extension MediaTileViewController: MediaPresentationContextProvider {
return self.navigationController?.navigationBar.generateSnapshot(in: coordinateSpace)
}
}
// MARK: - MediaTileViewControllerDelegate
public protocol MediaTileViewControllerDelegate: AnyObject {
func presentdetailViewController(_ detailViewController: UIViewController, animated: Bool)
func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool)
}

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -684,6 +684,11 @@
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
"MEDIA_TAB_TITLE" = "Media";
"DOCUMENT_TAB_TITLE" = "Documents";
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
/* The name for the emoji category 'Activities' */
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
/* The name for the emoji category 'Animals & Nature' */

@ -58,4 +58,8 @@ 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")
}

Loading…
Cancel
Save