diff --git a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 5b4efd2c..035381c5 100644
--- a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -68,8 +68,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/kean/Nuke",
       "state" : {
-        "revision" : "311016d972aa751ae8ab0cd5897422ebe7db0501",
-        "version" : "12.7.3"
+        "revision" : "0ead44350d2737db384908569c012fe67c421e4d",
+        "version" : "12.8.0"
       }
     },
     {
diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift
index 62029115..dfcb9d0e 100644
--- a/IceCubesApp/App/AppRegistry.swift
+++ b/IceCubesApp/App/AppRegistry.swift
@@ -24,6 +24,8 @@ extension View {
         AccountDetailView(account: account)
       case let .accountSettingsWithAccount(account, appAccount):
         AccountSettingsView(account: account, appAccount: appAccount)
+      case let .accountMediaGridView(account, initialMedia):
+        AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia)
       case let .statusDetail(id):
         StatusDetailView(statusId: id)
       case let .statusDetailWithStatus(status):
diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift
index 33cfa9cf..415948ac 100644
--- a/Packages/Account/Sources/Account/AccountDetailView.swift
+++ b/Packages/Account/Sources/Account/AccountDetailView.swift
@@ -75,6 +75,22 @@ public struct AccountDetailView: View {
         if viewModel.selectedTab == .statuses {
           pinnedPostsView
         }
+        if viewModel.selectedTab == .media {
+          HStack {
+            Label("Media Grid", systemImage: "square.grid.2x2")
+            Spacer()
+            Image(systemName: "chevron.right")
+          }
+            .onTapGesture {
+              if let account = viewModel.account {
+                routerPath.navigate(to: .accountMediaGridView(account: account,
+                                                              initialMediaStatuses: viewModel.statusesMedias))
+              }
+            }
+          #if !os(visionOS)
+            .listRowBackground(theme.primaryBackgroundColor)
+          #endif
+        }
         StatusesListView(fetcher: viewModel,
                          client: client,
                          routerPath: routerPath)
diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift
index 80d2270d..fae5d3c9 100644
--- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift
+++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift
@@ -84,6 +84,9 @@ import SwiftUI
   private var tabTask: Task<Void, Never>?
 
   private(set) var statuses: [Status] = []
+  var statusesMedias: [MediaStatus] {
+    statuses.filter{ !$0.mediaAttachments.isEmpty }.flatMap{ $0.asMediaStatus}
+  }
 
   var boosts: [Status] = []
 
diff --git a/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift b/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift
new file mode 100644
index 00000000..ddf0af33
--- /dev/null
+++ b/Packages/Account/Sources/Account/MediaGrid/AccountDetailMediaGridView.swift
@@ -0,0 +1,116 @@
+import SwiftUI
+import DesignSystem
+import NukeUI
+import Env
+import MediaUI
+import Models
+import Network
+
+@MainActor
+public struct AccountDetailMediaGridView: View {
+  @Environment(Theme.self) private var theme
+  @Environment(RouterPath.self) private var routerPath
+  @Environment(Client.self) private var client
+  @Environment(QuickLook.self) private var quickLook
+  
+  let account: Account
+  @State var mediaStatuses: [MediaStatus]
+  
+  public init(account: Account, initialMediaStatuses: [MediaStatus]) {
+    self.account = account
+    self.mediaStatuses = initialMediaStatuses
+  }
+  
+  public var body: some View {
+    ScrollView(.vertical) {
+      LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4),
+                          .init(.flexible(minimum: 100), spacing: 4),
+                          .init(.flexible(minimum: 100), spacing: 4)],
+                spacing: 4) {
+        ForEach(mediaStatuses) { status in
+          GeometryReader { proxy in
+            if let url = status.attachment.url {
+              Group {
+                switch status.attachment.supportedType {
+                case .image:
+                  LazyImage(url: url, transaction: Transaction(animation: .easeIn)) { state in
+                    if let image = state.image {
+                        image
+                        .resizable()
+                        .scaledToFill()
+                        .frame(width: proxy.size.width, height: proxy.size.width)
+                    } else {
+                      ProgressView()
+                        .frame(width: proxy.size.width, height: proxy.size.width)
+                    }
+                  }
+                  .processors([.resize(size: proxy.size)])
+                  .transition(.opacity)
+                case .gifv, .video:
+                  MediaUIAttachmentVideoView(viewModel: .init(url: url))
+                case .none:
+                  EmptyView()
+                case .some(.audio):
+                  EmptyView()
+                }
+              }
+              .onTapGesture {
+                routerPath.navigate(to: .statusDetailWithStatus(status: status.status))
+              }
+              .contextMenu {
+                Button {
+                  quickLook.prepareFor(selectedMediaAttachment: status.attachment,
+                                       mediaAttachments: status.status.mediaAttachments)
+                } label: {
+                  Label("Open Media", systemImage: "photo")
+                }
+                MediaUIShareLink(url: url, type: status.attachment.supportedType == .image ? .image : .av)
+                Button {
+                  Task {
+                    let transferable = MediaUIImageTransferable(url: url)
+                    UIPasteboard.general.image = UIImage(data: await transferable.fetchData())
+                  }
+                } label: {
+                  Label("status.media.contextmenu.copy", systemImage: "doc.on.doc")
+                }
+                Button {
+                  UIPasteboard.general.url = url
+                } label: {
+                  Label("status.action.copy-link", systemImage: "link")
+                }
+              }
+            }
+          }
+          .clipped()
+          .aspectRatio(1, contentMode: .fit)
+        }
+        
+        VStack {
+          Spacer()
+          NextPageView {
+            try await fetchNextPage()
+          }
+          Spacer()
+        }
+      }
+    }
+    .navigationTitle(account.displayName ?? "")
+    #if !os(visionOS)
+    .scrollContentBackground(.hidden)
+    .background(theme.primaryBackgroundColor)
+    #endif
+  }
+  
+  private func fetchNextPage() async throws {
+    let client = client
+    let newStatuses: [Status] =
+    try await client.get(endpoint: Accounts.statuses(id: account.id,
+                                                      sinceId: mediaStatuses.last?.id,
+                                                       tag: nil,
+                                                       onlyMedia: true,
+                                                       excludeReplies: true,
+                                                       excludeReblogs: true,
+                                                       pinned: nil))
+    mediaStatuses.append(contentsOf: newStatuses.flatMap{ $0.asMediaStatus })
+  }
+}
diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift
index ae4574cd..bb212390 100644
--- a/Packages/DesignSystem/Package.swift
+++ b/Packages/DesignSystem/Package.swift
@@ -19,7 +19,7 @@ let package = Package(
   dependencies: [
     .package(name: "Models", path: "../Models"),
     .package(name: "Env", path: "../Env"),
-    .package(url: "https://github.com/kean/Nuke", exact: "12.7.3"),
+    .package(url: "https://github.com/kean/Nuke", exact: "12.8.0"),
     .package(url: "https://github.com/divadretlaw/EmojiText", exact: "4.0.0"),
   ],
   targets: [
diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift
index 0f76def0..ac6f6ff7 100644
--- a/Packages/Env/Sources/Env/Router.swift
+++ b/Packages/Env/Sources/Env/Router.swift
@@ -9,6 +9,7 @@ public enum RouterDestination: Hashable {
   case accountDetail(id: String)
   case accountDetailWithAccount(account: Account)
   case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
+  case accountMediaGridView(account: Account, initialMediaStatuses: [MediaStatus])
   case statusDetail(id: String)
   case statusDetailWithStatus(status: Status)
   case remoteStatusDetail(url: URL)
diff --git a/Packages/MediaUI/Sources/MediaUI/DisplayType.swift b/Packages/MediaUI/Sources/MediaUI/DisplayType.swift
index 371e56a5..e92ac9ac 100644
--- a/Packages/MediaUI/Sources/MediaUI/DisplayType.swift
+++ b/Packages/MediaUI/Sources/MediaUI/DisplayType.swift
@@ -1,11 +1,11 @@
 import Models
 import SwiftUI
 
-enum DisplayType {
+public enum DisplayType {
   case image
   case av
 
-  init(from attachmentType: MediaAttachment.SupportedType) {
+  public init(from attachmentType: MediaAttachment.SupportedType) {
     switch attachmentType {
     case .image:
       self = .image
diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentImageView.swift b/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentImageView.swift
index cc5f9921..e43a4c20 100644
--- a/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentImageView.swift
+++ b/Packages/MediaUI/Sources/MediaUI/MediaUIAttachmentImageView.swift
@@ -2,12 +2,12 @@ import Models
 import NukeUI
 import SwiftUI
 
-struct MediaUIAttachmentImageView: View {
-  let url: URL
+public struct MediaUIAttachmentImageView: View {
+  public let url: URL
 
   @GestureState private var zoom = 1.0
 
-  var body: some View {
+  public var body: some View {
     MediaUIZoomableContainer {
       LazyImage(url: url) { state in
         if let image = state.image {
diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift b/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift
index 54e0e1f6..5ab0e198 100644
--- a/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift
+++ b/Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift
@@ -1,10 +1,15 @@
 import SwiftUI
 
-struct MediaUIShareLink: View, @unchecked Sendable {
+public struct MediaUIShareLink: View, @unchecked Sendable {
   let url: URL
   let type: DisplayType
 
-  var body: some View {
+  public init(url: URL, type: DisplayType) {
+    self.url = url
+    self.type = type
+  }
+  
+  public var body: some View {
     if type == .image {
       let transferable = MediaUIImageTransferable(url: url)
       ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
diff --git a/Packages/MediaUI/Sources/MediaUI/MediaUITransferableImage.swift b/Packages/MediaUI/Sources/MediaUI/MediaUITransferableImage.swift
index 27a1404b..78c46113 100644
--- a/Packages/MediaUI/Sources/MediaUI/MediaUITransferableImage.swift
+++ b/Packages/MediaUI/Sources/MediaUI/MediaUITransferableImage.swift
@@ -2,10 +2,14 @@ import CoreTransferable
 import SwiftUI
 import UIKit
 
-struct MediaUIImageTransferable: Codable, Transferable {
-  let url: URL
+public struct MediaUIImageTransferable: Codable, Transferable {
+  public let url: URL
 
-  func fetchData() async -> Data {
+  public init(url: URL) {
+    self.url = url
+  }
+  
+  public func fetchData() async -> Data {
     do {
       return try await URLSession.shared.data(from: url).0
     } catch {
@@ -13,7 +17,7 @@ struct MediaUIImageTransferable: Codable, Transferable {
     }
   }
 
-  static var transferRepresentation: some TransferRepresentation {
+  public static var transferRepresentation: some TransferRepresentation {
     DataRepresentation(exportedContentType: .jpeg) { transferable in
       await transferable.fetchData()
     }
diff --git a/Packages/Models/Sources/Models/MediaStatus.swift b/Packages/Models/Sources/Models/MediaStatus.swift
new file mode 100644
index 00000000..69088738
--- /dev/null
+++ b/Packages/Models/Sources/Models/MediaStatus.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+public struct MediaStatus: Sendable, Identifiable, Hashable {
+  public var id: String {
+    attachment.id
+  }
+  
+  public let status: Status
+  public let attachment: MediaAttachment
+  
+  public init(status: Status, attachment: MediaAttachment) {
+    self.status = status
+    self.attachment = attachment
+  }
+}
diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift
index 6edd2f35..449809b5 100644
--- a/Packages/Models/Sources/Models/Status.swift
+++ b/Packages/Models/Sources/Models/Status.swift
@@ -77,6 +77,10 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
   public var isHidden: Bool {
     filtered?.first?.filter.filterAction == .hide
   }
+  
+  public var asMediaStatus: [MediaStatus] {
+    mediaAttachments.map{ .init(status: self, attachment: $0)}
+  }
 
   public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
     self.id = id