~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
191 lines · 7.9 KB Raw History
import Foundation
import Nuke

struct PrioritizedRequests {
    let high: [ImageRequest]
    let normal: [ImageRequest]
    let low: [ImageRequest]

    var all: [ImageRequest] {
        high + normal + low
    }
}

enum ImagePrefetchPlanning {
    // MARK: - Carousel

    /// Tiered prefetch URLs for a gallery photo carousel.
    ///
    /// - Page 0 (on appear): fullsize #0, #1 at high
    /// - Page 1: fullsize #2 at high, all thumbs at normal
    /// - Page 2+: next fullsize at high, remaining fullsize at normal
    static func carouselPrefetchRequests(
        photos: [(thumb: String, fullsize: String)],
        currentPage: Int
    ) -> PrioritizedRequests {
        guard !photos.isEmpty, currentPage >= 0, currentPage < photos.count else {
            return PrioritizedRequests(high: [], normal: [], low: [])
        }

        var high: [ImageRequest] = []
        var normal: [ImageRequest] = []
        let low: [ImageRequest] = []

        if currentPage == 0 {
            // Prefetch first 2 fullsize
            for i in 0 ..< min(2, photos.count) {
                if let url = URL(string: photos[i].fullsize) {
                    high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
                }
            }
        } else if currentPage == 1 {
            // Fullsize #2 at high
            if photos.count > 2, let url = URL(string: photos[2].fullsize) {
                high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
            }
            // All thumbs at normal
            for photo in photos {
                if let url = URL(string: photo.thumb) {
                    normal.append(ImageRequest(url: url, priority: .normal))
                }
            }
        } else {
            // Page 2+: next fullsize at high, rest at normal
            for i in (currentPage + 1) ..< photos.count {
                if let url = URL(string: photos[i].fullsize) {
                    let priority: ImageRequest.Priority = i == currentPage + 1 ? .high : .normal
                    if priority == .high {
                        high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
                    } else {
                        normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
                    }
                }
            }
        }

        return PrioritizedRequests(high: high, normal: normal, low: low)
    }

    // MARK: - Feed

    /// Prefetch first image (thumb + fullsize) of upcoming galleries in the feed.
    ///
    /// - i+1: thumb at high, fullsize at high
    /// - i+2: thumb at high, fullsize at normal
    /// - i+3: thumb at high, fullsize at low
    static func feedPrefetchRequests(
        galleries: [(firstThumb: String?, firstFullsize: String?)],
        currentIndex: Int
    ) -> PrioritizedRequests {
        guard currentIndex >= 0, currentIndex < galleries.count else {
            return PrioritizedRequests(high: [], normal: [], low: [])
        }

        var high: [ImageRequest] = []
        var normal: [ImageRequest] = []
        var low: [ImageRequest] = []

        for offset in 1 ... 3 {
            let idx = currentIndex + offset
            guard idx < galleries.count else { break }
            let gallery = galleries[idx]

            // Thumbs are tiny — always high priority
            if let thumb = gallery.firstThumb, let url = URL(string: thumb) {
                high.append(ImageRequest(url: url, priority: .high))
            }

            if let fullsize = gallery.firstFullsize, let url = URL(string: fullsize) {
                switch offset {
                case 1:
                    high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
                case 2:
                    normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
                default:
                    low.append(ImageRequest(url: url, priority: .low, options: .disableDiskCacheWrites))
                }
            }
        }

        return PrioritizedRequests(high: high, normal: normal, low: low)
    }

    // MARK: - Stories

    /// Prefetch story images with priority queue:
    /// - Thumbs (all reachable authors) — high (tiny payload, used by the
    ///   StoryViewer's fullsize-loading placeholder; needs to be cache-hot to
    ///   avoid a 1-frame flash on transition)
    /// 1. Next 2 stories of current author — fullsize at high
    /// 2. First story of next author — fullsize at high
    /// 3. Rest of current author's stack — fullsize at normal
    /// 4. Next 2 stories of author-after-next — fullsize at normal
    /// 5. First story of next 2 authors beyond — fullsize at low
    static func storyPrefetchRequests(
        currentStories: [(thumb: String, fullsize: String)],
        currentStoryIndex: Int,
        nextAuthorStories: [(thumb: String, fullsize: String)]?,
        secondNextAuthorStories: [(thumb: String, fullsize: String)]?,
        thirdNextFirstStory: (thumb: String, fullsize: String)?,
        fourthNextFirstStory: (thumb: String, fullsize: String)?
    ) -> PrioritizedRequests {
        var high: [ImageRequest] = []
        var normal: [ImageRequest] = []
        var low: [ImageRequest] = []

        // Thumbs are tiny — prefetch every reachable thumb at high priority so
        // the StoryViewer's fullsize-loading placeholder always hits the
        // synchronous Nuke cache (mirrors `feedPrefetchRequests`' policy).
        let allThumbStrings: [String] = currentStories.map(\.thumb)
            + (nextAuthorStories?.map(\.thumb) ?? [])
            + (secondNextAuthorStories?.map(\.thumb) ?? [])
            + [thirdNextFirstStory?.thumb, fourthNextFirstStory?.thumb].compactMap(\.self)
        for thumbStr in allThumbStrings {
            if let url = URL(string: thumbStr) {
                high.append(ImageRequest(url: url, priority: .high))
            }
        }

        // 1. Next 2 stories of current author — high
        let storyEnd = min(currentStoryIndex + 3, currentStories.count)
        for i in stride(from: currentStoryIndex + 1, to: storyEnd, by: 1) where i < currentStories.count {
            if let url = URL(string: currentStories[i].fullsize) {
                high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
            }
        }

        // 2. First story of next author — high
        if let first = nextAuthorStories?.first, let url = URL(string: first.fullsize) {
            high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
        }

        // 3. Rest of current author's stack — normal
        let restStart = currentStoryIndex + 3
        if restStart < currentStories.count {
            for i in restStart ..< currentStories.count {
                if let url = URL(string: currentStories[i].fullsize) {
                    normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
                }
            }
        }

        // 4. Next 2 stories of author-after-next — normal
        if let stories = secondNextAuthorStories {
            for i in 0 ..< min(2, stories.count) {
                if let url = URL(string: stories[i].fullsize) {
                    normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
                }
            }
        }

        // 5. First story of next 2 authors beyond — low
        if let story = thirdNextFirstStory, let url = URL(string: story.fullsize) {
            low.append(ImageRequest(url: url, priority: .low, options: .disableDiskCacheWrites))
        }
        if let story = fourthNextFirstStory, let url = URL(string: story.fullsize) {
            low.append(ImageRequest(url: url, priority: .low, options: .disableDiskCacheWrites))
        }

        return PrioritizedRequests(high: high, normal: normal, low: low)
    }
}