diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj
index 97f4e8859..1ea6d1a83 100644
--- a/Signal.xcodeproj/project.pbxproj
+++ b/Signal.xcodeproj/project.pbxproj
@@ -179,7 +179,6 @@
 		34C6B0AB1FA0E46F00D35993 /* test-mp3.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A71FA0E46F00D35993 /* test-mp3.mp3 */; };
 		34C6B0AC1FA0E46F00D35993 /* test-mp4.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A81FA0E46F00D35993 /* test-mp4.mp4 */; };
 		34C6B0AE1FA0E4AA00D35993 /* test-jpg.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0AD1FA0E4AA00D35993 /* test-jpg.jpg */; };
-		34CA1C271F7156F300E51C51 /* MessageDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */; };
 		34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; };
 		34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; };
 		34CF0787203E6B78005C4D61 /* busy_tone_ansi.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0783203E6B77005C4D61 /* busy_tone_ansi.caf */; };
@@ -341,7 +340,6 @@
 		45B74A892044AAB600CD42F8 /* circles-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A702044AAB500CD42F8 /* circles-quiet.aifc */; };
 		45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A722044AAB600CD42F8 /* synth.aifc */; };
 		45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A732044AAB600CD42F8 /* input-quiet.aifc */; };
-		45B9EE9C200E91FB005D2F2D /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */; };
 		45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; };
 		45BC829D1FD9C4B400011CF3 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */; };
 		45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BD60811DE9547E00A8F436 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
@@ -358,6 +356,9 @@
 		45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; };
 		45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */; };
 		45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; };
+		45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */; };
+		45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */; };
+		45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */; };
 		45F59A082028E4FB00E8D2B0 /* OWSAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* OWSAudioSession.swift */; };
 		45F59A0A2029140500E8D2B0 /* OWSVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F59A092029140500E8D2B0 /* OWSVideoPlayer.swift */; };
 		45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F659721E1BD99C00444429 /* CallKitCallUIAdaptee.swift */; };
@@ -958,6 +959,7 @@
 		45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = "<group>"; };
 		45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = "<group>"; };
 		45F170D51E315310003FC1F2 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
+		45F32C1D205718B000A300D5 /* MediaPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaPageViewController.swift; path = Signal/src/ViewControllers/MediaPageViewController.swift; sourceTree = SOURCE_ROOT; };
 		45F3AEB51DFDE7900080CE33 /* AvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = "<group>"; };
 		45F59A092029140500E8D2B0 /* OWSVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSVideoPlayer.swift; sourceTree = "<group>"; };
 		45F659721E1BD99C00444429 /* CallKitCallUIAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallUIAdaptee.swift; sourceTree = "<group>"; };
@@ -1538,6 +1540,7 @@
 				34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */,
 				34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */,
 				34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */,
+				45F32C1D205718B000A300D5 /* MediaPageViewController.swift */,
 				45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */,
 				45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */,
 				34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */,
@@ -3148,7 +3151,6 @@
 				34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
 				452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */,
 				34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
-				34CA1C271F7156F300E51C51 /* MessageDetailViewController.swift in Sources */,
 				45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */,
 				34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
 				34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */,
@@ -3160,6 +3162,7 @@
 				34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */,
 				34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
 				3497DBEC1ECE257500DB2605 /* OWSCountryMetadata.m in Sources */,
+				45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
 				34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
 				34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */,
 				34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */,
@@ -3177,6 +3180,7 @@
 				340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
 				4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */,
 				340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */,
+				45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
 				340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
 				452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
 				45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
@@ -3187,8 +3191,8 @@
 				34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
 				45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
 				45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */,
-				45B9EE9C200E91FB005D2F2D /* MediaDetailViewController.m in Sources */,
 				458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
+				45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
 				34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
 				FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */,
 				76EB054018170B33006006FC /* AppDelegate.m in Sources */,
diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
index ae5cd69fc..c705d2aa2 100644
--- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
+++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
@@ -2028,10 +2028,7 @@ typedef enum : NSUInteger {
 
     [self dismissKeyBoard];
 
-    UIWindow *window = [UIApplication sharedApplication].keyWindow;
-    CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
     MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream
-                                                                                       fromRect:convertedRect
                                                                                        viewItem:viewItem];
     [vc presentFromViewController:self replacingView:imageView];
 }
@@ -2045,11 +2042,7 @@ typedef enum : NSUInteger {
     OWSAssert(attachmentStream);
 
     [self dismissKeyBoard];
-    UIWindow *window = [UIApplication sharedApplication].keyWindow;
-    CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
-
     MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream
-                                                                                       fromRect:convertedRect
                                                                                        viewItem:viewItem];
     [vc presentFromViewController:self replacingView:imageView];
 }
diff --git a/Signal/src/ViewControllers/MediaDetailViewController.h b/Signal/src/ViewControllers/MediaDetailViewController.h
index 61f1694b7..d704e6481 100644
--- a/Signal/src/ViewControllers/MediaDetailViewController.h
+++ b/Signal/src/ViewControllers/MediaDetailViewController.h
@@ -14,10 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
 
 // If viewItem is non-null, long press will show a menu controller.
 - (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
-                                fromRect:(CGRect)rect
                                 viewItem:(ConversationViewItem *_Nullable)viewItem;
 
-- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect;
+- (instancetype)initWithAttachment:(SignalAttachment *)attachment;
 
 - (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view;
 
diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m
index 4fefc8bcc..4c56a18b1 100644
--- a/Signal/src/ViewControllers/MediaDetailViewController.m
+++ b/Signal/src/ViewControllers/MediaDetailViewController.m
@@ -85,29 +85,28 @@ NS_ASSUME_NONNULL_BEGIN
 @implementation MediaDetailViewController
 
 - (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
-                                fromRect:(CGRect)rect
                                 viewItem:(ConversationViewItem *_Nullable)viewItem
 {
     self = [super initWithNibName:nil bundle:nil];
-
-    if (self) {
-        self.attachmentStream = attachmentStream;
-        self.originRect = rect;
-        self.viewItem = viewItem;
+    if (!self) {
+        return self;
     }
 
+    self.attachmentStream = attachmentStream;
+    self.viewItem = viewItem;
+
     return self;
 }
 
-- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect
+- (instancetype)initWithAttachment:(SignalAttachment *)attachment
 {
     self = [super initWithNibName:nil bundle:nil];
-
-    if (self) {
-        self.attachment = attachment;
-        self.originRect = rect;
+    if (!self) {
+        return self;
     }
 
+    self.attachment = attachment;
+
     return self;
 }
 
@@ -188,7 +187,7 @@ NS_ASSUME_NONNULL_BEGIN
     // The alternative would be that content would shift when the navbars hide.
     self.extendedLayoutIncludesOpaqueBars = YES;
 
-    // TODO better title.
+    // FIXME better title.
     self.title = @"Attachment";
 
     self.navigationItem.leftBarButtonItem =
@@ -309,8 +308,6 @@ NS_ASSUME_NONNULL_BEGIN
     presentationView.layer.magnificationFilter = kCAFilterTrilinear;
     presentationView.contentMode = UIViewContentModeScaleAspectFit;
 
-    [self applyInitialMediaViewConstraints];
-
     if (self.isVideo) {
         PlayerProgressBar *videoProgressBar = [PlayerProgressBar new];
         videoProgressBar.delegate = self;
@@ -400,6 +397,7 @@ NS_ASSUME_NONNULL_BEGIN
         [NSLayoutConstraint deactivateConstraints:self.presentationViewConstraints];
     }
 
+    OWSAssert(!CGRectEqualToRect(CGRectZero, self.originRect));
     CGRect convertedRect = [self.presentationView.superview convertRect:self.originRect
                                                                fromView:[UIApplication sharedApplication].keyWindow];
 
@@ -714,9 +712,18 @@ NS_ASSUME_NONNULL_BEGIN
 
 #pragma mark - Presentation
 
-- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view
+- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)replacingView
 {
-    self.replacingView = view;
+    self.replacingView = replacingView;
+
+    UIWindow *window = [UIApplication sharedApplication].keyWindow;
+    CGRect convertedRect = [replacingView convertRect:replacingView.bounds toView:window];
+    self.originRect = convertedRect;
+
+    // loadView hasn't necesarily been called yet.
+    [self loadViewIfNeeded];
+    [self applyInitialMediaViewConstraints];
+
     UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self];
 
     // UIModalPresentationCustom retains the current view context behind our VC, allowing us to manually
diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift
index 05fe651a7..6e1f5b44a 100644
--- a/Signal/src/ViewControllers/MessageDetailViewController.swift
+++ b/Signal/src/ViewControllers/MessageDetailViewController.swift
@@ -750,14 +750,12 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
     // MARK: MediaDetailPresenter
 
     public func presentDetails(mediaMessageView: MediaMessageView, fromView: UIView) {
-        let window = UIApplication.shared.keyWindow
-        let convertedRect = fromView.convert(fromView.bounds, to: window)
         guard let attachmentStream = self.attachmentStream else {
             owsFail("attachment stream unexpectedly nil")
             return
         }
 
-        let mediaDetailViewController = MediaDetailViewController(attachmentStream: attachmentStream, from: convertedRect, viewItem: self.viewItem)
+        let mediaDetailViewController = MediaDetailViewController(attachmentStream: attachmentStream, viewItem: self.viewItem)
         mediaDetailViewController.present(from: self, replacing: fromView)
     }
 }