Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions App/Misc/HTMLRenderingHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import HTMLReader

extension HTMLDocument {

// MARK: - Constants

/// Number of post images to load immediately before deferring to lazy loading.
/// IMPORTANT: This value must match IMMEDIATELY_LOADED_IMAGE_COUNT constant in RenderView.js
private static let immediatelyLoadedImageCount = 10

/// 1x1 transparent GIF used as placeholder for lazy-loaded images
private static let transparentPixelPlaceholder = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"

// MARK: - HTML Processing Methods

/// Finds links that appear to be to Bluesky posts and adds a `data-bluesky-post` attribute to those links.
func addAttributeToBlueskyLinks() {
for a in nodes(matchingSelector: "a[href *= 'bsky.app']") {
Expand Down Expand Up @@ -138,22 +149,56 @@ extension HTMLDocument {
- Turns all non-smiley `<img src=>` elements into `<a data-awful='image'>src</a>` elements (if linkifyNonSmiles == true).
- Adds .awful-smile to smilie elements.
- Rewrites URLs for some external image hosts that have changed domains and/or URL schemes.
- Defers loading of post content images beyond the first 10 (lazy loading).
*/
func processImgTags(shouldLinkifyNonSmilies: Bool) {
var postContentImageCount = 0

for img in nodes(matchingSelector: "img") {
guard
let src = img["src"],
let url = URL(string: src)
else { continue }

let isSmilie = isSmilieURL(url)

if isSmilie {
img.toggleClass("awful-smile")
} else if let postimageURL = fixPostimageURL(url) {
img["src"] = postimageURL.absoluteString
} else if let waffleURL = randomwaffleURLForWaffleimagesURL(url) {
img["src"] = waffleURL.absoluteString
} else {
// Check if this is an avatar (has class="avatar")
let isAvatar = img["class"]?.contains("avatar") ?? false

// Skip attachment.php files (require auth, handled elsewhere)
let isAttachment = url.lastPathComponent == "attachment.php"

// Apply URL fixes first to get the final URL
var finalURL = src
if let postimageURL = fixPostimageURL(url) {
finalURL = postimageURL.absoluteString
} else if let waffleURL = randomwaffleURLForWaffleimagesURL(url) {
finalURL = waffleURL.absoluteString
}

// Check if this is a data: URI (inline data that shouldn't be lazy-loaded)
let isDataURI = finalURL.starts(with: "data:")

// Determine whether to load immediately or defer based on image type and count
if !isAvatar && !isAttachment && !isDataURI {
// This is a post content image (not avatar, not smilie, not attachment)
postContentImageCount += 1

if postContentImageCount > Self.immediatelyLoadedImageCount {
// Defer loading for images beyond the immediately loaded count (browser handles lazy loading)
img["loading"] = "lazy"
img["src"] = finalURL
} else {
// Load immediately
img["src"] = finalURL
}
} else {
// Avatars, attachments, and data URIs always load immediately
img["src"] = finalURL
}
}

if shouldLinkifyNonSmilies, !isSmilie {
Expand Down
Loading
Loading