diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index cc9d38a72..265836a9c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -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 = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; + 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; @@ -1206,6 +1209,7 @@ 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = ""; }; 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Session/Media Viewing & Editing/AllMediaViewController.swift b/Session/Media Viewing & Editing/AllMediaViewController.swift new file mode 100644 index 000000000..6090b7404 --- /dev/null +++ b/Session/Media Viewing & Editing/AllMediaViewController.swift @@ -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) + } +} + diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift new file mode 100644 index 000000000..af790fb89 --- /dev/null +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -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) +} diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index bad7fc350..2bc61de8d 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -18,12 +18,19 @@ public class MediaGalleryViewModel { case loadNewer } + // MARK: Media type + public enum MediaType { + case media + case document + } + // MARK: - Variables public let threadId: String public let threadVariant: SessionThread.Variant private var focusedAttachmentId: String? public private(set) var focusedIndexPath: IndexPath? + public var mediaType: MediaType /// This value is the current state of an album view private var cachedInteractionIdBefore: Atomic<[Int64: Int64]> = Atomic([:]) @@ -54,6 +61,7 @@ public class MediaGalleryViewModel { threadId: String, threadVariant: SessionThread.Variant, isPagedData: Bool, + mediaType: MediaType, pageSize: Int = 1, focusedAttachmentId: String? = nil, performInitialQuerySync: Bool = false @@ -62,6 +70,7 @@ public class MediaGalleryViewModel { self.threadVariant = threadVariant self.focusedAttachmentId = focusedAttachmentId self.pagedDataObserver = nil + self.mediaType = mediaType guard isPagedData else { return } @@ -80,7 +89,7 @@ public class MediaGalleryViewModel { ) ], joinSQL: Item.joinSQL, - filterSQL: Item.filterSQL(threadId: threadId), + filterSQL: Item.filterSQL(threadId: threadId, mediaType: self.mediaType), orderSQL: Item.galleryOrderSQL, dataQuery: Item.baseQuery(orderSQL: Item.galleryOrderSQL), onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in @@ -243,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 = TypedTableAlias() let attachment: TypedTableAlias = 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 } diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index ec290ea7e..061dd010d 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -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) } diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index 1085b574a..44d1bb4e3 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -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) +} diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 415dd41d3..55b3ce36b 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 64e3a9c83..7ddb8d8b7 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 0de94349d..a2773a8e1 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 8a310885b..229763cfe 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index bea2c1277..844c6ba4d 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 345cbc2e6..1c6a67899 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index c9298f93c..de76b147e 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 324a0702d..f63162780 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 27c64084d..5529c1aff 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 2ae8ccb16..751389352 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index a0f7b3120..a9c3f5283 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 58c2486c5..63ff7e6f0 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 4a809aa69..96680dc58 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 2aacfc5d1..f941dac33 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 5d2070325..25b500602 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 12553bb60..58632d39a 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 6f6cff3ca..ca329e64c 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index d47e96467..52e8a5d87 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 6a9d0aae9..551199e8b 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index aff668e23..382942b65 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 2125e6eea..a685e2014 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -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' */ diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 0352a3a29..3e3c21440 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -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' */ diff --git a/SignalUtilitiesKit/Utilities/CommonStrings.swift b/SignalUtilitiesKit/Utilities/CommonStrings.swift index b3c7f6c88..d67f49534 100644 --- a/SignalUtilitiesKit/Utilities/CommonStrings.swift +++ b/SignalUtilitiesKit/Utilities/CommonStrings.swift @@ -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") }