session-ios/Session/Media Viewing & Editing/MediaPageViewController.swift

1025 lines
40 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
2018-11-14 00:02:48 +01:00
import PromiseKit
import SessionUIKit
import SessionMessagingKit
import SignalUtilitiesKit
class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController {
class DynamicallySizedView: UIView {
override var intrinsicContentSize: CGSize { CGSize.zero }
}
fileprivate var mediaInteractiveDismiss: MediaInteractiveDismiss?
public let viewModel: MediaGalleryViewModel
private var dataChangeObservable: DatabaseCancellable?
private var initialPage: MediaDetailViewController
private var cachedPages: [Int64: [MediaGalleryViewModel.Item: MediaDetailViewController]] = [:]
public var currentViewController: MediaDetailViewController {
return viewControllers!.first as! MediaDetailViewController
}
public var currentItem: MediaGalleryViewModel.Item {
return currentViewController.galleryItem
}
public func setCurrentItem(_ item: MediaGalleryViewModel.Item, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) {
guard let galleryPage = self.buildGalleryPage(galleryItem: item) else {
2018-11-26 20:31:03 +01:00
owsFailDebug("unexpectedly unable to build new gallery page")
return
}
2018-11-14 00:02:48 +01:00
updateTitle(item: item)
updateCaption(item: item)
setViewControllers([galleryPage], direction: direction, animated: isAnimated)
updateFooterBarButtonItems(isPlayingVideo: false)
updateMediaRail(item: item)
}
private let showAllMediaButton: Bool
private let sliderEnabled: Bool
init(
viewModel: MediaGalleryViewModel,
initialItem: MediaGalleryViewModel.Item,
options: [MediaGalleryOption]
) {
self.viewModel = viewModel
self.showAllMediaButton = options.contains(.showAllMediaButton)
self.sliderEnabled = options.contains(.sliderEnabled)
self.initialPage = MediaDetailViewController(galleryItem: initialItem)
super.init(
transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: [ .interPageSpacing: 20 ]
)
self.cachedPages[initialItem.interactionId] = [initialItem: self.initialPage]
self.initialPage.delegate = self
self.dataSource = self
self.delegate = self
self.modalPresentationStyle = .overFullScreen
self.transitioningDelegate = self
self.setViewControllers([initialPage], direction: .forward, animated: false, completion: nil)
}
@available(*, unavailable, message: "Unimplemented")
required init?(coder: NSCoder) {
2018-08-27 16:21:03 +02:00
notImplemented()
}
deinit {
2018-08-23 16:37:34 +02:00
Logger.debug("deinit")
}
2018-11-14 17:09:24 +01:00
// MARK: - Subview
private var hasAppeared: Bool = false
override var canBecomeFirstResponder: Bool { hasAppeared }
2018-11-14 17:09:24 +01:00
override var inputAccessoryView: UIView? {
return bottomContainer
}
// MARK: - Bottom Bar
2018-11-08 17:28:07 +01:00
var bottomContainer: UIView!
var footerBar: UIToolbar = {
let result: UIToolbar = UIToolbar()
result.clipsToBounds = true // hide 1px top-border
result.tintColor = Colors.text
result.barTintColor = Colors.navigationBarBackground
result.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: UIBarMetrics.default)
result.setShadowImage(UIImage(), forToolbarPosition: .any)
result.isTranslucent = false
result.backgroundColor = Colors.navigationBarBackground
return result
}()
2018-11-14 17:09:24 +01:00
let captionContainerView: CaptionContainerView = CaptionContainerView()
var galleryRailView: GalleryRailView = GalleryRailView()
2018-11-14 00:02:48 +01:00
2018-11-14 17:09:24 +01:00
var pagerScrollView: UIScrollView!
2018-11-14 00:02:48 +01:00
2018-11-14 17:09:24 +01:00
// MARK: UIViewController overrides
2018-11-14 00:02:48 +01:00
override func viewDidLoad() {
super.viewDidLoad()
// Navigation
let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton))
self.navigationItem.leftBarButtonItem = backButton
2018-10-31 02:20:53 +01:00
self.navigationItem.titleView = portraitHeaderView
if showAllMediaButton {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: MediaStrings.allMedia, style: .plain, target: self, action: #selector(didPressAllMediaButton))
}
// Even though bars are opaque, we want content to be layed out behind them.
// The bars might obscure part of the content, but they can easily be hidden by tapping
// The alternative would be that content would shift when the navbars hide.
self.extendedLayoutIncludesOpaqueBars = true
self.automaticallyAdjustsScrollViewInsets = false
// Disable the interactivePopGestureRecognizer as we want to be able to swipe between
// different pages
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
self.mediaInteractiveDismiss = MediaInteractiveDismiss(targetViewController: self)
self.mediaInteractiveDismiss?.addGestureRecognizer(to: view)
// Get reference to paged content which lives in a scrollView created by the superclass
// We show/hide this content during presentation
for view in self.view.subviews {
if let pagerScrollView = view as? UIScrollView {
pagerScrollView.contentInsetAdjustmentBehavior = .never
self.pagerScrollView = pagerScrollView
}
}
// Hack to avoid "page" bouncing when not in gallery view.
// e.g. when getting to media details via message details screen, there's only
// one "Page" so the bounce doesn't make sense.
pagerScrollView.isScrollEnabled = sliderEnabled
pagerScrollViewContentOffsetObservation = pagerScrollView.observe(\.contentOffset, options: [.new]) { [weak self] _, change in
2018-11-08 17:28:07 +01:00
guard let strongSelf = self else { return }
strongSelf.pagerScrollView(strongSelf.pagerScrollView, contentOffsetDidChange: change)
}
// Views
2020-01-20 04:44:51 +01:00
pagerScrollView.backgroundColor = Colors.navigationBarBackground
2020-01-20 04:44:51 +01:00
view.backgroundColor = Colors.navigationBarBackground
2018-11-14 17:09:24 +01:00
captionContainerView.delegate = self
updateCaptionContainerVisibility()
2018-11-08 17:28:07 +01:00
galleryRailView.isHidden = true
2018-11-14 00:02:48 +01:00
galleryRailView.delegate = self
2018-11-14 17:09:24 +01:00
galleryRailView.autoSetDimension(.height, toSize: 72)
footerBar.autoSetDimension(.height, toSize: 44)
2018-11-14 00:02:48 +01:00
let bottomContainer: DynamicallySizedView = DynamicallySizedView()
bottomContainer.clipsToBounds = true
bottomContainer.autoresizingMask = .flexibleHeight
2019-12-12 06:49:48 +01:00
bottomContainer.backgroundColor = Colors.navigationBarBackground
self.bottomContainer = bottomContainer
2018-11-14 00:02:48 +01:00
2018-11-14 17:09:24 +01:00
let bottomStack = UIStackView(arrangedSubviews: [captionContainerView, galleryRailView, footerBar])
2018-11-08 17:28:07 +01:00
bottomStack.axis = .vertical
bottomStack.isLayoutMarginsRelativeArrangement = true
2018-11-08 17:28:07 +01:00
bottomContainer.addSubview(bottomStack)
bottomStack.autoPinEdgesToSuperviewEdges()
let galleryRailBlockingView: UIView = UIView()
galleryRailBlockingView.backgroundColor = Colors.navigationBarBackground
bottomStack.addSubview(galleryRailBlockingView)
galleryRailBlockingView.pin(.top, to: .bottom, of: footerBar)
galleryRailBlockingView.pin(.left, to: .left, of: bottomStack)
galleryRailBlockingView.pin(.right, to: .right, of: bottomStack)
galleryRailBlockingView.pin(.bottom, to: .bottom, of: bottomStack)
updateTitle(item: currentItem)
2018-11-14 17:09:24 +01:00
updateCaption(item: currentItem)
updateMediaRail(item: currentItem)
updateFooterBarButtonItems(isPlayingVideo: false)
// Gestures
let verticalSwipe = UISwipeGestureRecognizer(target: self, action: #selector(didSwipeView))
verticalSwipe.direction = [.up, .down]
view.addGestureRecognizer(verticalSwipe)
2019-12-12 06:49:48 +01:00
let navigationBar = navigationController!.navigationBar
navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = false
navigationBar.barTintColor = Colors.navigationBarBackground
// 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()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
hasAppeared = true
becomeFirstResponder()
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Stop observing database changes
dataChangeObservable?.cancel()
resignFirstResponder()
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges()
}
@objc func applicationDidResignActive(_ notification: Notification) {
// Stop observing database changes
dataChangeObservable?.cancel()
}
2018-11-14 17:09:24 +01:00
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let isLandscape = size.width > size.height
self.navigationItem.titleView = isLandscape ? nil : self.portraitHeaderView
}
override func didReceiveMemoryWarning() {
Logger.info("")
super.didReceiveMemoryWarning()
self.cachedPages = [:]
}
2018-11-08 17:28:07 +01:00
// MARK: KVO
var pagerScrollViewContentOffsetObservation: NSKeyValueObservation?
func pagerScrollView(_ pagerScrollView: UIScrollView, contentOffsetDidChange change: NSKeyValueObservedChange<CGPoint>) {
guard let newValue = change.newValue else {
owsFailDebug("newValue was unexpectedly nil")
return
}
let width = pagerScrollView.frame.size.width
guard width > 0 else {
return
}
let ratioComplete = abs((newValue.x - width) / width)
2018-11-14 17:09:24 +01:00
captionContainerView.updatePagerTransition(ratioComplete: ratioComplete)
}
// MARK: View Helpers
public func willBePresentedAgain() {
updateFooterBarButtonItems(isPlayingVideo: false)
}
public func wasPresented() {
let currentViewController = self.currentViewController
if currentViewController.galleryItem.isVideo {
currentViewController.playVideo()
}
}
private var shouldHideToolbars: Bool = false {
didSet {
guard oldValue != shouldHideToolbars else { return }
// Hiding the status bar affects the positioning of the navbar. We don't want to show
// that in an animation, it's better to just have everythign "flit" in/out
UIApplication.shared.setStatusBarHidden(shouldHideToolbars, with: .none)
self.navigationController?.setNavigationBarHidden(shouldHideToolbars, animated: false)
UIView.animate(withDuration: 0.1) {
self.currentViewController.setShouldHideToolbars(self.shouldHideToolbars)
2018-11-08 17:28:07 +01:00
self.bottomContainer.isHidden = self.shouldHideToolbars
}
}
}
2018-11-14 17:09:24 +01:00
// MARK: Bar Buttons
2018-11-15 21:30:46 +01:00
lazy var shareBarButton: UIBarButtonItem = {
let shareBarButton = UIBarButtonItem(
barButtonSystemItem: .action,
target: self,
action: #selector(didPressShare)
)
shareBarButton.tintColor = Colors.text
2018-11-14 17:09:24 +01:00
return shareBarButton
}()
2018-11-15 21:30:46 +01:00
lazy var deleteBarButton: UIBarButtonItem = {
let deleteBarButton = UIBarButtonItem(
barButtonSystemItem: .trash,
target: self,
action: #selector(didPressDelete)
)
deleteBarButton.tintColor = Colors.text
2018-11-14 17:09:24 +01:00
return deleteBarButton
}()
func buildFlexibleSpace() -> UIBarButtonItem {
return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
}
2018-11-22 05:20:04 +01:00
lazy var videoPlayBarButton: UIBarButtonItem = {
let videoPlayBarButton = UIBarButtonItem(
barButtonSystemItem: .play,
target: self,
action: #selector(didPressPlayBarButton)
)
videoPlayBarButton.tintColor = Colors.text
2018-11-14 17:09:24 +01:00
return videoPlayBarButton
}()
2018-11-22 05:20:04 +01:00
lazy var videoPauseBarButton: UIBarButtonItem = {
let videoPauseBarButton = UIBarButtonItem(
barButtonSystemItem: .pause,
target: self,
action: #selector(didPressPauseBarButton)
)
videoPauseBarButton.tintColor = Colors.text
2018-11-14 17:09:24 +01:00
return videoPauseBarButton
}()
private func updateFooterBarButtonItems(isPlayingVideo: Bool) {
self.footerBar.setItems(
[
shareBarButton,
buildFlexibleSpace(),
(self.currentItem.isVideo && isPlayingVideo ? self.videoPauseBarButton : nil),
(self.currentItem.isVideo && !isPlayingVideo ? self.videoPlayBarButton : nil),
(self.currentItem.isVideo ? buildFlexibleSpace() : nil),
deleteBarButton
].compactMap { $0 },
animated: false
)
}
func updateMediaRail(item: MediaGalleryViewModel.Item) {
galleryRailView.configureCellViews(
album: (self.viewModel.albumData[item.interactionId] ?? []),
focusedItem: currentItem,
cellViewBuilder: { _ in return GalleryRailCellView() }
)
}
// MARK: - Updating
private func startObservingChanges() {
// Start observing for data changes
dataChangeObservable = Storage.shared.start(
viewModel.observableAlbumData,
onError: { _ in },
onChange: { [weak self] albumData in
// The defaul scheduler emits changes on the main thread
self?.handleUpdates(albumData)
}
)
}
private func handleUpdates(_ updatedViewData: [MediaGalleryViewModel.Item]) {
// Determine if we swapped albums (if so we don't need to do anything else)
guard updatedViewData.contains(where: { $0.interactionId == currentItem.interactionId }) else {
if let updatedInteractionId: Int64 = updatedViewData.first?.interactionId {
self.viewModel.updateAlbumData(updatedViewData, for: updatedInteractionId)
}
return
}
// Clear the cached pages that no longer match
let interactionId: Int64 = currentItem.interactionId
let updatedCachedPages: [MediaGalleryViewModel.Item: MediaDetailViewController] = cachedPages[interactionId]
.defaulting(to: [:])
.filter { key, _ -> Bool in updatedViewData.contains(key) }
// If there are no more items in the album then dismiss the screen
guard
!updatedViewData.isEmpty,
let oldIndex: Int = self.viewModel.albumData[interactionId]?.firstIndex(of: currentItem)
else {
self.dismissSelf(animated: true)
return
}
// Update the caches
self.viewModel.updateAlbumData(updatedViewData, for: interactionId)
self.cachedPages[interactionId] = updatedCachedPages
// If the current item is still available then do nothing else
guard updatedCachedPages[currentItem] == nil else { return }
// If the current item was modified within the current update then reload it (just in case)
if let updatedCurrentItem: MediaGalleryViewModel.Item = updatedViewData.first(where: { item in item.attachment.id == currentItem.attachment.id }) {
setCurrentItem(updatedCurrentItem, direction: .forward, animated: false)
2018-11-14 00:02:48 +01:00
return
}
// Determine the next index (if it's less than 0 then pop the screen)
let nextIndex: Int = min(oldIndex, (updatedViewData.count - 1))
guard nextIndex >= 0 else {
self.dismissSelf(animated: true)
return
}
self.setCurrentItem(
updatedViewData[nextIndex],
direction: (nextIndex < oldIndex ?
.reverse :
.forward
),
animated: true
)
2018-11-14 00:02:48 +01:00
}
// MARK: - Actions
2018-11-08 17:28:07 +01:00
@objc public func didPressAllMediaButton(sender: Any) {
2018-11-08 17:28:07 +01:00
currentViewController.stopAnyVideo()
// If the screen wasn't presented or it was presented from a location which isn't the
// MediaTileViewController then just pop/dismiss the screen
guard
let presentingNavController: UINavigationController = (self.presentingViewController as? UINavigationController),
!(presentingNavController.viewControllers.last is MediaTileViewController)
else {
guard self.navigationController?.viewControllers.count == 1 else {
self.navigationController?.popViewController(animated: true)
return
}
self.dismiss(animated: true)
2018-11-08 17:28:07 +01:00
return
}
// Otherwise if we came via the conversation screen we need to push a new
// instance of MediaTileViewController
let tileViewController: MediaTileViewController = MediaGalleryViewModel.createTileViewController(
threadId: self.viewModel.threadId,
threadVariant: self.viewModel.threadVariant,
focusedAttachmentId: currentItem.attachment.id,
performInitialQuerySync: true
)
let navController: MediaGalleryNavigationController = MediaGalleryNavigationController()
navController.viewControllers = [tileViewController]
navController.modalPresentationStyle = .overFullScreen
navController.transitioningDelegate = tileViewController
self.navigationController?.present(navController, animated: true)
2018-11-08 17:28:07 +01:00
}
@objc public func didSwipeView(sender: Any) {
2018-11-08 17:28:07 +01:00
self.dismissSelf(animated: true)
}
@objc public func didPressDismissButton(_ sender: Any) {
dismissSelf(animated: true)
}
@objc
public func didPressShare(_ sender: Any) {
guard let currentViewController = self.viewControllers?[0] as? MediaDetailViewController else {
2018-08-27 16:27:48 +02:00
owsFailDebug("currentViewController was unexpectedly nil")
return
}
guard let originalFilePath: String = currentViewController.galleryItem.attachment.originalFilePath else {
return
}
let shareVC = UIActivityViewController(activityItems: [ URL(fileURLWithPath: originalFilePath) ], applicationActivities: nil)
2022-02-24 05:35:35 +01:00
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = self.view
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
}
Merge branch 'feature/session-id-blinding-part-2' into feature/database-refactor # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Closed Groups/EditClosedGroupVC.swift # Session/Closed Groups/NewClosedGroupVC.swift # Session/Conversations/Context Menu/ContextMenuVC+Action.swift # Session/Conversations/Context Menu/ContextMenuVC.swift # Session/Conversations/ConversationMessageMapping.swift # Session/Conversations/ConversationSearch.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewItem.h # Session/Conversations/ConversationViewItem.m # Session/Conversations/ConversationViewModel.m # Session/Conversations/Input View/InputView.swift # Session/Conversations/Input View/MentionSelectionView.swift # Session/Conversations/LongTextViewController.swift # Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift # Session/Conversations/Message Cells/MessageCell.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Conversations/Settings/OWSConversationSettingsViewController.m # Session/Conversations/Views & Modals/ConversationTitleView.swift # Session/Conversations/Views & Modals/DownloadAttachmentModal.swift # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Conversations/Views & Modals/LinkPreviewModal.swift # Session/Conversations/Views & Modals/MessagesTableView.swift # Session/Conversations/Views & Modals/URLModal.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/Message Requests/MessageRequestsViewController.swift # Session/Media Viewing & Editing/MediaDetailViewController.m # Session/Media Viewing & Editing/MediaPageViewController.swift # Session/Meta/AppDelegate.m # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/Signal-Bridging-Header.h # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Notifications/AppNotifications.swift # Session/Open Groups/JoinOpenGroupVC.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SeedModal.swift # Session/Settings/SettingsVC.swift # Session/Settings/ShareLogsModal.swift # Session/Shared/ConversationCell.swift # Session/Shared/UserSelectionVC.swift # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MentionUtilities.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Database/OWSPrimaryStorage.m # SessionMessagingKit/Database/SSKPreferences.swift # SessionMessagingKit/Database/Storage+Contacts.swift # SessionMessagingKit/Database/Storage+Jobs.swift # SessionMessagingKit/Database/Storage+Messaging.swift # SessionMessagingKit/Database/Storage+OpenGroups.swift # SessionMessagingKit/Database/TSDatabaseView.m # SessionMessagingKit/File Server/FileServerAPIV2.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/AttachmentUploadJob.swift # SessionMessagingKit/Jobs/JobQueue.swift # SessionMessagingKit/Jobs/MessageReceiveJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/NotifyPNServerJob.swift # SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift # SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift # SessionMessagingKit/Messages/Message+Destination.swift # SessionMessagingKit/Messages/Signal/TSIncomingMessage.h # SessionMessagingKit/Messages/Signal/TSIncomingMessage.m # SessionMessagingKit/Messages/Signal/TSInfoMessage.h # SessionMessagingKit/Messages/Signal/TSInfoMessage.m # SessionMessagingKit/Messages/Signal/TSInteraction.h # SessionMessagingKit/Messages/Signal/TSInteraction.m # SessionMessagingKit/Messages/Signal/TSMessage.h # SessionMessagingKit/Messages/Signal/TSMessage.m # SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift # SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift # SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift # SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift # SessionMessagingKit/Sending & Receiving/Mentions/MentionsManager.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Storage.swift # SessionMessagingKit/Threads/Notification+Thread.swift # SessionMessagingKit/Threads/TSContactThread.h # SessionMessagingKit/Threads/TSContactThread.m # SessionMessagingKit/Threads/TSGroupModel.h # SessionMessagingKit/Threads/TSGroupModel.m # SessionMessagingKit/Threads/TSGroupThread.m # SessionMessagingKit/Utilities/General.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionSnodeKit/OnionRequestAPI+Encryption.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SnodeAPI.swift # SessionSnodeKit/SnodeMessage.swift # SessionSnodeKit/Storage+SnodeAPI.swift # SessionSnodeKit/Storage.swift # SessionUtilitiesKit/General/Array+Utilities.swift # SessionUtilitiesKit/General/Dictionary+Utilities.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/Set+Utilities.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Utilities/Optional+Utilities.swift # SessionUtilitiesKit/Utilities/Sodium+Conversion.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift # SignalUtilitiesKit/Messaging/FullTextSearcher.swift # SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift # SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift # SignalUtilitiesKit/To Do/OWSProfileManager.m # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # SignalUtilitiesKit/Utilities/UIView+OWS.swift
2022-06-08 06:29:51 +02:00
shareVC.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in
if let activityError = activityError {
SNLog("Failed to share with activityError: \(activityError)")
Merge branch 'feature/session-id-blinding-part-2' into feature/database-refactor # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Closed Groups/EditClosedGroupVC.swift # Session/Closed Groups/NewClosedGroupVC.swift # Session/Conversations/Context Menu/ContextMenuVC+Action.swift # Session/Conversations/Context Menu/ContextMenuVC.swift # Session/Conversations/ConversationMessageMapping.swift # Session/Conversations/ConversationSearch.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewItem.h # Session/Conversations/ConversationViewItem.m # Session/Conversations/ConversationViewModel.m # Session/Conversations/Input View/InputView.swift # Session/Conversations/Input View/MentionSelectionView.swift # Session/Conversations/LongTextViewController.swift # Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift # Session/Conversations/Message Cells/MessageCell.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Conversations/Settings/OWSConversationSettingsViewController.m # Session/Conversations/Views & Modals/ConversationTitleView.swift # Session/Conversations/Views & Modals/DownloadAttachmentModal.swift # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Conversations/Views & Modals/LinkPreviewModal.swift # Session/Conversations/Views & Modals/MessagesTableView.swift # Session/Conversations/Views & Modals/URLModal.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/Message Requests/MessageRequestsViewController.swift # Session/Media Viewing & Editing/MediaDetailViewController.m # Session/Media Viewing & Editing/MediaPageViewController.swift # Session/Meta/AppDelegate.m # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/Signal-Bridging-Header.h # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Notifications/AppNotifications.swift # Session/Open Groups/JoinOpenGroupVC.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SeedModal.swift # Session/Settings/SettingsVC.swift # Session/Settings/ShareLogsModal.swift # Session/Shared/ConversationCell.swift # Session/Shared/UserSelectionVC.swift # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MentionUtilities.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Database/OWSPrimaryStorage.m # SessionMessagingKit/Database/SSKPreferences.swift # SessionMessagingKit/Database/Storage+Contacts.swift # SessionMessagingKit/Database/Storage+Jobs.swift # SessionMessagingKit/Database/Storage+Messaging.swift # SessionMessagingKit/Database/Storage+OpenGroups.swift # SessionMessagingKit/Database/TSDatabaseView.m # SessionMessagingKit/File Server/FileServerAPIV2.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/AttachmentUploadJob.swift # SessionMessagingKit/Jobs/JobQueue.swift # SessionMessagingKit/Jobs/MessageReceiveJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/NotifyPNServerJob.swift # SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift # SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift # SessionMessagingKit/Messages/Message+Destination.swift # SessionMessagingKit/Messages/Signal/TSIncomingMessage.h # SessionMessagingKit/Messages/Signal/TSIncomingMessage.m # SessionMessagingKit/Messages/Signal/TSInfoMessage.h # SessionMessagingKit/Messages/Signal/TSInfoMessage.m # SessionMessagingKit/Messages/Signal/TSInteraction.h # SessionMessagingKit/Messages/Signal/TSInteraction.m # SessionMessagingKit/Messages/Signal/TSMessage.h # SessionMessagingKit/Messages/Signal/TSMessage.m # SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift # SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift # SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift # SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift # SessionMessagingKit/Sending & Receiving/Mentions/MentionsManager.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Storage.swift # SessionMessagingKit/Threads/Notification+Thread.swift # SessionMessagingKit/Threads/TSContactThread.h # SessionMessagingKit/Threads/TSContactThread.m # SessionMessagingKit/Threads/TSGroupModel.h # SessionMessagingKit/Threads/TSGroupModel.m # SessionMessagingKit/Threads/TSGroupThread.m # SessionMessagingKit/Utilities/General.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionSnodeKit/OnionRequestAPI+Encryption.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SnodeAPI.swift # SessionSnodeKit/SnodeMessage.swift # SessionSnodeKit/Storage+SnodeAPI.swift # SessionSnodeKit/Storage.swift # SessionUtilitiesKit/General/Array+Utilities.swift # SessionUtilitiesKit/General/Dictionary+Utilities.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/Set+Utilities.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Utilities/Optional+Utilities.swift # SessionUtilitiesKit/Utilities/Sodium+Conversion.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift # SignalUtilitiesKit/Messaging/FullTextSearcher.swift # SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift # SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift # SignalUtilitiesKit/To Do/OWSProfileManager.m # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # SignalUtilitiesKit/Utilities/UIView+OWS.swift
2022-06-08 06:29:51 +02:00
}
else if completed {
SNLog("Did share with activityType: \(activityType.debugDescription)")
}
Merge branch 'feature/session-id-blinding-part-2' into feature/database-refactor # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Closed Groups/EditClosedGroupVC.swift # Session/Closed Groups/NewClosedGroupVC.swift # Session/Conversations/Context Menu/ContextMenuVC+Action.swift # Session/Conversations/Context Menu/ContextMenuVC.swift # Session/Conversations/ConversationMessageMapping.swift # Session/Conversations/ConversationSearch.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewItem.h # Session/Conversations/ConversationViewItem.m # Session/Conversations/ConversationViewModel.m # Session/Conversations/Input View/InputView.swift # Session/Conversations/Input View/MentionSelectionView.swift # Session/Conversations/LongTextViewController.swift # Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift # Session/Conversations/Message Cells/MessageCell.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Conversations/Settings/OWSConversationSettingsViewController.m # Session/Conversations/Views & Modals/ConversationTitleView.swift # Session/Conversations/Views & Modals/DownloadAttachmentModal.swift # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Conversations/Views & Modals/LinkPreviewModal.swift # Session/Conversations/Views & Modals/MessagesTableView.swift # Session/Conversations/Views & Modals/URLModal.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/Message Requests/MessageRequestsViewController.swift # Session/Media Viewing & Editing/MediaDetailViewController.m # Session/Media Viewing & Editing/MediaPageViewController.swift # Session/Meta/AppDelegate.m # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/Signal-Bridging-Header.h # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Notifications/AppNotifications.swift # Session/Open Groups/JoinOpenGroupVC.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SeedModal.swift # Session/Settings/SettingsVC.swift # Session/Settings/ShareLogsModal.swift # Session/Shared/ConversationCell.swift # Session/Shared/UserSelectionVC.swift # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MentionUtilities.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Database/OWSPrimaryStorage.m # SessionMessagingKit/Database/SSKPreferences.swift # SessionMessagingKit/Database/Storage+Contacts.swift # SessionMessagingKit/Database/Storage+Jobs.swift # SessionMessagingKit/Database/Storage+Messaging.swift # SessionMessagingKit/Database/Storage+OpenGroups.swift # SessionMessagingKit/Database/TSDatabaseView.m # SessionMessagingKit/File Server/FileServerAPIV2.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/AttachmentUploadJob.swift # SessionMessagingKit/Jobs/JobQueue.swift # SessionMessagingKit/Jobs/MessageReceiveJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/NotifyPNServerJob.swift # SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift # SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift # SessionMessagingKit/Messages/Message+Destination.swift # SessionMessagingKit/Messages/Signal/TSIncomingMessage.h # SessionMessagingKit/Messages/Signal/TSIncomingMessage.m # SessionMessagingKit/Messages/Signal/TSInfoMessage.h # SessionMessagingKit/Messages/Signal/TSInfoMessage.m # SessionMessagingKit/Messages/Signal/TSInteraction.h # SessionMessagingKit/Messages/Signal/TSInteraction.m # SessionMessagingKit/Messages/Signal/TSMessage.h # SessionMessagingKit/Messages/Signal/TSMessage.m # SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift # SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift # SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift # SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift # SessionMessagingKit/Sending & Receiving/Mentions/MentionsManager.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Storage.swift # SessionMessagingKit/Threads/Notification+Thread.swift # SessionMessagingKit/Threads/TSContactThread.h # SessionMessagingKit/Threads/TSContactThread.m # SessionMessagingKit/Threads/TSGroupModel.h # SessionMessagingKit/Threads/TSGroupModel.m # SessionMessagingKit/Threads/TSGroupThread.m # SessionMessagingKit/Utilities/General.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionSnodeKit/OnionRequestAPI+Encryption.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SnodeAPI.swift # SessionSnodeKit/SnodeMessage.swift # SessionSnodeKit/Storage+SnodeAPI.swift # SessionSnodeKit/Storage.swift # SessionUtilitiesKit/General/Array+Utilities.swift # SessionUtilitiesKit/General/Dictionary+Utilities.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/Set+Utilities.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Utilities/Optional+Utilities.swift # SessionUtilitiesKit/Utilities/Sodium+Conversion.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift # SignalUtilitiesKit/Messaging/FullTextSearcher.swift # SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift # SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift # SignalUtilitiesKit/To Do/OWSProfileManager.m # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # SignalUtilitiesKit/Utilities/UIView+OWS.swift
2022-06-08 06:29:51 +02:00
guard
let activityType = activityType,
activityType == .saveToCameraRoll,
currentViewController.galleryItem.interactionVariant == .standardIncoming,
self.viewModel.threadVariant == .contact
else { return }
Storage.shared.write { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: self.viewModel.threadId) else {
return
}
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .mediaSaved(
timestamp: UInt64(currentViewController.galleryItem.interactionTimestampMs)
)
),
interactionId: nil, // Show no interaction for the current user
in: thread
)
}
}
self.present(shareVC, animated: true, completion: nil)
}
@objc public func didPressDelete(_ sender: Any) {
let itemToDelete: MediaGalleryViewModel.Item = self.currentItem
let actionSheet: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(
title: "delete_message_for_me".localized(),
style: .destructive
) { _ in
Storage.shared.writeAsync { db in
_ = try Attachment
.filter(id: itemToDelete.attachment.id)
.deleteAll(db)
// Add the garbage collection job to delete orphaned attachment files
JobRunner.add(
db,
job: Job(
variant: .garbageCollection,
behaviour: .runOnce,
details: GarbageCollectionJob.Details(
typesToCollect: [.orphanedAttachmentFiles]
)
)
)
// Delete any interactions which had all of their attachments removed
_ = try Interaction
.filter(id: itemToDelete.interactionId)
.having(Interaction.interactionAttachments.isEmpty)
.deleteAll(db)
}
}
actionSheet.addAction(OWSAlerts.cancelAction)
actionSheet.addAction(deleteAction)
self.presentAlert(actionSheet)
}
// MARK: - Video interaction
@objc public func didPressPlayBarButton() {
guard let currentViewController = self.viewControllers?.first as? MediaDetailViewController else {
SNLog("currentViewController was unexpectedly nil")
return
}
currentViewController.didPressPlayBarButton()
}
@objc public func didPressPauseBarButton() {
guard let currentViewController = self.viewControllers?.first as? MediaDetailViewController else {
SNLog("currentViewController was unexpectedly nil")
return
}
currentViewController.didPressPauseBarButton()
}
// MARK: UIPageViewControllerDelegate
2018-11-08 17:28:07 +01:00
var pendingViewController: MediaDetailViewController?
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
2018-08-23 16:37:34 +02:00
Logger.debug("")
assert(pendingViewControllers.count == 1)
pendingViewControllers.forEach { viewController in
2018-11-08 17:28:07 +01:00
guard let pendingViewController = viewController as? MediaDetailViewController else {
2018-08-27 16:27:48 +02:00
owsFailDebug("unexpected mediaDetailViewController: \(viewController)")
return
}
2018-11-08 17:28:07 +01:00
self.pendingViewController = pendingViewController
2018-11-09 16:18:11 +01:00
if let pendingCaptionText = pendingViewController.galleryItem.captionForDisplay, pendingCaptionText.count > 0 {
2018-11-14 17:09:24 +01:00
self.captionContainerView.pendingText = pendingCaptionText
2018-11-08 17:28:07 +01:00
} else {
2018-11-14 17:09:24 +01:00
self.captionContainerView.pendingText = nil
2018-11-08 17:28:07 +01:00
}
// Ensure upcoming page respects current toolbar status
2018-11-08 17:28:07 +01:00
pendingViewController.setShouldHideToolbars(self.shouldHideToolbars)
}
}
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted: Bool) {
2018-08-23 16:37:34 +02:00
Logger.debug("")
assert(previousViewControllers.count == 1)
previousViewControllers.forEach { viewController in
guard let previousPage = viewController as? MediaDetailViewController else {
2018-08-27 16:27:48 +02:00
owsFailDebug("unexpected mediaDetailViewController: \(viewController)")
return
}
// Do any cleanup for the no-longer visible view controller
if transitionCompleted {
2018-11-08 17:28:07 +01:00
pendingViewController = nil
// This can happen when trying to page past the last (or first) view controller
// In that case, we don't want to change the captionView.
if (previousPage != currentViewController) {
2018-11-14 17:09:24 +01:00
captionContainerView.completePagerTransition()
2018-11-08 17:28:07 +01:00
}
updateTitle(item: currentItem)
updateMediaRail(item: currentItem)
previousPage.zoomOut(animated: false)
previousPage.stopAnyVideo()
updateFooterBarButtonItems(isPlayingVideo: false)
2018-11-14 17:09:24 +01:00
} else {
captionContainerView.pendingText = nil
}
}
}
// MARK: UIPageViewControllerDataSource
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let mediaViewController: MediaDetailViewController = viewController as? MediaDetailViewController else {
return nil
}
// First check if there is another item in the current album
let interactionId: Int64 = mediaViewController.galleryItem.interactionId
if
let currentAlbum: [MediaGalleryViewModel.Item] = self.viewModel.albumData[interactionId],
let index: Int = currentAlbum.firstIndex(of: mediaViewController.galleryItem),
index > 0,
let previousPage: MediaDetailViewController = buildGalleryPage(galleryItem: currentAlbum[index - 1])
{
return previousPage
}
// Then check if there is an interaction before the current album interaction
guard let interactionIdAfter: Int64 = self.viewModel.interactionIdAfter[interactionId] else { return nil }
// Cache and retrieve the new album items
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdAfter)
guard
!newAlbumItems.isEmpty,
let previousPage: MediaDetailViewController = buildGalleryPage(
galleryItem: newAlbumItems[newAlbumItems.count - 1]
)
else {
// Invalid state, restart the observer
startObservingChanges()
return nil
}
// Swap out the database observer
dataChangeObservable?.cancel()
viewModel.replaceAlbumObservation(toObservationFor: interactionIdAfter)
startObservingChanges()
return previousPage
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let mediaViewController: MediaDetailViewController = viewController as? MediaDetailViewController else {
return nil
}
// First check if there is another item in the current album
let interactionId: Int64 = mediaViewController.galleryItem.interactionId
if
let currentAlbum: [MediaGalleryViewModel.Item] = self.viewModel.albumData[interactionId],
let index: Int = currentAlbum.firstIndex(of: mediaViewController.galleryItem),
index < (currentAlbum.count - 1),
let nextPage: MediaDetailViewController = buildGalleryPage(galleryItem: currentAlbum[index + 1])
{
return nextPage
}
// Then check if there is an interaction before the current album interaction
guard let interactionIdBefore: Int64 = self.viewModel.interactionIdBefore[interactionId] else { return nil }
// Cache and retrieve the new album items
let newAlbumItems: [MediaGalleryViewModel.Item] = viewModel.loadAndCacheAlbumData(for: interactionIdBefore)
guard
!newAlbumItems.isEmpty,
let nextPage: MediaDetailViewController = buildGalleryPage(galleryItem: newAlbumItems[0])
else {
// Invalid state, restart the observer
startObservingChanges()
return nil
}
// Swap out the database observer
dataChangeObservable?.cancel()
viewModel.replaceAlbumObservation(toObservationFor: interactionIdBefore)
startObservingChanges()
return nextPage
}
private func buildGalleryPage(galleryItem: MediaGalleryViewModel.Item) -> MediaDetailViewController? {
if let cachedPage: MediaDetailViewController = cachedPages[galleryItem.interactionId]?[galleryItem] {
return cachedPage
}
cachedPages[galleryItem.interactionId] = (cachedPages[galleryItem.interactionId] ?? [:])
.setting(galleryItem, MediaDetailViewController(galleryItem: galleryItem, delegate: self))
return cachedPages[galleryItem.interactionId]?[galleryItem]
}
public func dismissSelf(animated isAnimated: Bool, completion: (() -> Void)? = nil) {
// If we have presented a MediaTileViewController from this screen then it will continue
// to observe media changes and if all the items in the album this screen is showing are
// deleted it will attempt to auto-dismiss
guard self.presentedViewController == nil else { return }
// Swapping mediaView for presentationView will be perceptible if we're not zoomed out all the way.
// currentVC
currentViewController.zoomOut(animated: true)
2018-03-23 22:14:37 +01:00
currentViewController.stopAnyVideo()
2018-03-19 18:44:59 +01:00
self.navigationController?.view.isUserInteractionEnabled = false
self.navigationController?.dismiss(animated: true, completion: { [weak self] in
if !IsLandscapeOrientationEnabled() {
UIDevice.current.ows_setOrientation(.portrait)
}
UIApplication.shared.isStatusBarHidden = false
self?.navigationController?.presentingViewController?.setNeedsStatusBarAppearanceUpdate()
completion?()
})
}
// MARK: MediaDetailViewControllerDelegate
public func mediaDetailViewControllerDidTapMedia(_ mediaDetailViewController: MediaDetailViewController) {
2018-08-23 16:37:34 +02:00
Logger.debug("")
self.shouldHideToolbars = !self.shouldHideToolbars
}
public func mediaDetailViewController(_ mediaDetailViewController: MediaDetailViewController, isPlayingVideo: Bool) {
guard mediaDetailViewController == currentViewController else {
2018-08-23 16:37:34 +02:00
Logger.verbose("ignoring stale delegate.")
return
}
self.shouldHideToolbars = isPlayingVideo
self.updateFooterBarButtonItems(isPlayingVideo: isPlayingVideo)
}
2018-03-20 19:37:30 +01:00
// MARK: - Dynamic Header
2018-03-20 19:37:30 +01:00
private lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
2018-10-31 02:20:53 +01:00
lazy private var portraitHeaderNameLabel: UILabel = {
2018-03-20 19:37:30 +01:00
let label = UILabel()
2019-12-12 06:49:48 +01:00
label.textColor = Colors.text
label.font = .systemFont(ofSize: Values.mediumFontSize)
2018-03-20 19:37:30 +01:00
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.8
return label
}()
2018-10-31 02:20:53 +01:00
lazy private var portraitHeaderDateLabel: UILabel = {
2018-03-20 19:37:30 +01:00
let label = UILabel()
2019-12-12 06:49:48 +01:00
label.textColor = Colors.text
label.font = .systemFont(ofSize: Values.verySmallFontSize)
2018-03-20 19:37:30 +01:00
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.8
2018-03-20 19:37:30 +01:00
return label
}()
2018-10-31 02:20:53 +01:00
private lazy var portraitHeaderView: UIView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 0
stackView.distribution = .fillProportionally
stackView.addArrangedSubview(portraitHeaderNameLabel)
stackView.addArrangedSubview(portraitHeaderDateLabel)
let containerView = UIView()
containerView.layoutMargins = UIEdgeInsets(top: 2, left: 8, bottom: 4, right: 8)
containerView.addSubview(stackView)
stackView.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual)
stackView.autoPinEdge(toSuperviewMargin: .trailing, relation: .greaterThanOrEqual)
stackView.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual)
stackView.autoPinEdge(toSuperviewMargin: .leading, relation: .greaterThanOrEqual)
stackView.setContentHuggingHigh()
stackView.autoCenterInSuperview()
return containerView
}()
private func updateCaption(item: MediaGalleryViewModel.Item) {
2018-11-14 17:09:24 +01:00
captionContainerView.currentText = item.captionForDisplay
2018-11-09 23:26:12 +01:00
}
private func updateTitle(item: MediaGalleryViewModel.Item) {
let targetItem: MediaGalleryViewModel.Item = item
let threadVariant: SessionThread.Variant = self.viewModel.threadVariant
let name: String = {
switch targetItem.interactionVariant {
case .standardIncoming:
return Storage.shared
.read { db in
Profile.displayName(
db,
id: targetItem.interactionAuthorId,
threadVariant: threadVariant
)
}
.defaulting(to: Profile.truncated(id: targetItem.interactionAuthorId, truncating: .middle))
case .standardOutgoing:
return "MEDIA_GALLERY_SENDER_NAME_YOU".localized() //"Short sender label for media sent by you"
default:
owsFailDebug("Unsupported message variant: \(targetItem.interactionVariant)")
return ""
}
}()
2018-10-31 02:20:53 +01:00
portraitHeaderNameLabel.text = name
2018-03-20 19:37:30 +01:00
// use sent date
let date = Date(timeIntervalSince1970: (Double(targetItem.interactionTimestampMs) / 1000))
2018-03-20 19:37:30 +01:00
let formattedDate = dateFormatter.string(from: date)
2018-10-31 02:20:53 +01:00
portraitHeaderDateLabel.text = formattedDate
let landscapeHeaderFormat = NSLocalizedString("MEDIA_GALLERY_LANDSCAPE_TITLE_FORMAT", comment: "embeds {{sender name}} and {{sent datetime}}, e.g. 'Sarah on 10/30/18, 3:29'")
let landscapeHeaderText = String(format: landscapeHeaderFormat, name, formattedDate)
self.title = landscapeHeaderText
self.navigationItem.title = landscapeHeaderText
2018-03-20 19:37:30 +01:00
}
// MARK: - InteractivelyDismissableViewController
func performInteractiveDismissal(animated: Bool) {
dismissSelf(animated: true)
}
}
2018-11-08 17:28:07 +01:00
extension MediaGalleryViewModel.Item: GalleryRailItem {
2019-04-20 00:21:00 +02:00
public func buildRailItemView() -> UIView {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
getRailImage().map { [weak imageView] image in
guard let imageView = imageView else { return }
imageView.image = image
}.retainUntilComplete()
return imageView
2018-11-15 03:01:21 +01:00
}
2019-04-20 00:21:00 +02:00
public func getRailImage() -> Guarantee<UIImage> {
return Guarantee<UIImage> { fulfill in
self.thumbnailImage(async: { image in fulfill(image) })
2018-11-15 03:01:21 +01:00
}
}
public func isEqual(to other: GalleryRailItem?) -> Bool {
guard let otherItem: MediaGalleryViewModel.Item = other as? MediaGalleryViewModel.Item else { return false }
return (self == otherItem)
2018-11-15 03:01:21 +01:00
}
}
2018-11-14 00:02:48 +01:00
extension MediaPageViewController: GalleryRailViewDelegate {
func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) {
guard let targetItem = imageRailItem as? MediaGalleryViewModel.Item else {
2018-11-14 00:02:48 +01:00
owsFailDebug("unexpected imageRailItem: \(imageRailItem)")
return
}
self.setCurrentItem(
targetItem,
direction: (currentItem.attachmentAlbumIndex < targetItem.attachmentAlbumIndex ?
.forward :
.reverse
),
animated: true
)
2018-11-14 00:02:48 +01:00
}
}
2018-11-14 17:09:24 +01:00
extension MediaPageViewController: CaptionContainerViewDelegate {
2018-11-14 17:09:24 +01:00
func captionContainerViewDidUpdateText(_ captionContainerView: CaptionContainerView) {
updateCaptionContainerVisibility()
2018-11-08 17:28:07 +01:00
}
2018-11-14 17:09:24 +01:00
// MARK: Helpers
2018-11-14 17:09:24 +01:00
func updateCaptionContainerVisibility() {
if let currentText = captionContainerView.currentText, currentText.count > 0 {
captionContainerView.isHidden = false
return
}
2018-11-14 17:09:24 +01:00
if let pendingText = captionContainerView.pendingText, pendingText.count > 0 {
captionContainerView.isHidden = false
return
}
2018-11-14 17:09:24 +01:00
captionContainerView.isHidden = true
}
2018-11-08 17:28:07 +01:00
}
// MARK: - UIViewControllerTransitioningDelegate
extension MediaPageViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard self == presented || self.navigationController == presented else { return nil }
return MediaZoomAnimationController(galleryItem: currentItem)
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard self == dismissed || self.navigationController == dismissed else { return nil }
guard !self.viewModel.albumData.isEmpty else { return nil }
let animationController = MediaDismissAnimationController(galleryItem: currentItem, interactionController: mediaInteractiveDismiss)
mediaInteractiveDismiss?.interactiveDismissDelegate = animationController
return animationController
}
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MediaDismissAnimationController,
let interactionController = animator.interactionController,
interactionController.interactionInProgress
else {
return nil
}
return interactionController
}
}
// MARK: - MediaPresentationContextProvider
extension MediaPageViewController: MediaPresentationContextProvider {
func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {
let mediaView = currentViewController.mediaView
guard let mediaSuperview: UIView = mediaView.superview else { return nil }
let presentationFrame = coordinateSpace.convert(mediaView.frame, from: mediaSuperview)
return MediaPresentationContext(
mediaView: mediaView,
presentationFrame: presentationFrame,
cornerRadius: 0,
cornerMask: CACornerMask()
)
}
func snapshotOverlayView(in coordinateSpace: UICoordinateSpace) -> (UIView, CGRect)? {
return self.navigationController?.navigationBar.generateSnapshot(in: coordinateSpace)
}
}