Skip to content

feat: show image preview in topic list#386

Open
MufanQiu wants to merge 4 commits into
BugenZhao:mainfrom
MufanQiu:feat/topic-list-image-preview
Open

feat: show image preview in topic list#386
MufanQiu wants to merge 4 commits into
BugenZhao:mainfrom
MufanQiu:feat/topic-list-image-preview

Conversation

@MufanQiu

@MufanQiu MufanQiu commented Jun 30, 2026

Copy link
Copy Markdown

What

Add an opt-in "Image Preview" setting to the topic list. When enabled, each
topic row shows configurable thumbnail previews of images from the topic's main
floor, so users can decide whether to open a topic straight from the list —
similar to the official NGA app.

The setting lives under Settings → Appearance → Topic List and is off by
default
, so existing behavior is unchanged unless the user opts in.

How

  • Rust backend: a new async service topic_preview_images (in
    logic/service/src/topic.rs) fetches a topic's first post and extracts image
    URLs from its [img] spans, skipping videos. Results (including empty ones)
    are cached under a dedicated key, and the existing topic-details cache is
    reused before hitting the network, so topics aren't refetched on every launch.
    New request/response messages are added to Service.proto, and Topic gains
    a preview_image_urls field in DataModel.proto.
  • Frontend: a reusable fetchTopicPreviewImages view modifier lazily
    fetches previews per visible response (skipping shortcut-forum rows and topics
    that already have previews), dedupes per session, and retries on failure. The
    thumbnail strip is decorative and lets taps fall through to the row's
    navigation link so tapping anywhere opens the topic.
  • Configurable & extended: the preview image count (1–9) and height
    (40–240)
    are adjustable from settings at runtime (no rebuild needed), and the
    preview can be enabled independently for the search results and hot
    topics
    screens via their own toggles.

Screenshots

Settings Result in topic list
settings list

Testing

  • cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings pass.
  • Added unit tests for image-URL extraction and the cache-hit path.
  • Built and ran on the iOS Simulator and a physical device; verified previews
    appear only when enabled, rows without images are unaffected, tapping a row
    (including the image strip) opens the topic, and count/height update live.

Add an opt-in setting (off by default) under Topic list appearance. When
enabled, topic rows show up to 4 thumbnail previews of images from the main
floor, so users can decide whether to open a topic from the list.

A new async service `topic_preview_images` extracts image URLs from a topic's
first post on the Rust side; the topic list fetches them lazily per visible
response and caches the result on the Topic model. Shortcut-forum rows and
topics that already carry preview URLs are skipped.
Copilot AI review requested due to automatic review settings June 30, 2026 11:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in “Image Preview” feature for the topic list by extending the protobuf contract, implementing a Rust async service to extract first-post image URLs, and updating SwiftUI to fetch/cache and render up to 4 thumbnails per topic row.

Changes:

  • Extend protobuf APIs: add topic_preview_images async request/response and a Topic.preview_image_urls field.
  • Implement Rust get_topic_preview_images service that fetches topic page 1 and extracts [img] URLs (skipping video extensions).
  • Add iOS setting + UI rendering/fetching logic to show a decorative thumbnail strip in topic rows, plus zh-Hans localization.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
protos/Service.proto Adds TopicPreviewImagesRequest/Response and registers the async request variant.
protos/DataModel.proto Adds Topic.preview_image_urls to carry preview URLs to the app.
logic/service/src/topic.rs Adds span URL extraction helpers and the get_topic_preview_images async service.
logic/service/src/dispatch/mod.rs Routes topic_preview_images requests to the async handler.
logic/service/src/dispatch/handlers_async.rs Registers the new async handler and imports the service function.
app/Shared/Views/TopicRowView.swift Renders up to 4 thumbnail previews in topic rows (opt-in).
app/Shared/Views/TopicListView.swift Triggers lazy preview fetches per visible topic list response.
app/Shared/Views/PreferencesView.swift Adds the “Image Preview” toggle under Topic List appearance.
app/Shared/Storage/PreferencesStorage.swift Persists the topicListShowImagePreview preference (default off).
app/Shared/Localization/zh-Hans.lproj/Localizable.strings Adds zh-Hans translation for “Image Preview”.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/Shared/Views/TopicRowView.swift Outdated
Comment on lines +42 to +49
ForEach(Array(previewImageUrls.prefix(4)), id: \.self) { urlStr in
WebImage(url: URL(string: urlStr, relativeTo: URLs.attachmentBase))
.resizable()
.indicator(.activity)
.scaledToFill()
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
// The preview strip is decorative: let taps fall through to the row's
// NavigationLink so tapping the images (or their row) opens the topic.
.allowsHitTesting(false)
Comment thread app/Shared/Views/TopicListView.swift Outdated
Comment on lines +356 to +359
.onChange(of: dataSource.latestResponse) {
updateForumMeta($1)
fetchPreviewImagesIfNeeded($1)
}
Comment thread app/Shared/Views/TopicListView.swift Outdated
Comment on lines +392 to +403
logicCallAsync(.topicPreviewImages(.with {
$0.topicID = topic.id
}), errorToastModel: nil) { (response: TopicPreviewImagesResponse) in
guard !response.imageUrls.isEmpty else { return }
// Find and update the topic in the data source items.
if let index = dataSource.items.firstIndex(where: { $0.id == response.topicID }) {
withAnimation {
dataSource.items[index].previewImageUrls = response.imageUrls
}
}
}
}
}

/// Extract up to `limit` image URLs from a PostContent's spans.
/// Only extracts URLs from `[img]` tags, skipping videos (.mp4).
Comment thread logic/service/src/topic.rs Outdated
Comment on lines +826 to +840
// Try to get from cache first.
let cache_key = format!("{}/{}/page/1", TOPIC_DETAILS_PREFIX, topic_id);
if let Ok(Some(cached)) = CACHE.get_msg::<TopicDetailsResponse>(&cache_key)
&& let Some(first_post) = cached.replies.first()
{
let urls = extract_image_urls_from_spans(first_post.get_content().get_spans(), limit);
if !urls.is_empty() {
return Ok(TopicPreviewImagesResponse {
topic_id: topic_id.to_owned(),
image_urls: urls.into(),
..Default::default()
});
}
}

MufanQiu added 3 commits June 30, 2026 05:14
- Backend: cache the extracted preview image URLs (including empty results)
  under a dedicated key, so topics are not refetched on every cold start, and
  reuse the topic-details cache before hitting the network.
- Frontend: track per-session requested topic IDs so a topic (even one with no
  images) is not requested again while scrolling/refreshing.
- Add unit tests for URL extraction and the cache hit path.
- Settings: add a stepper for preview image count (1-9) and a slider for
  preview image height (40-120), both applied at runtime via @AppStorage.
- Extend image preview to the search results and hot topics screens, each
  gated by its own opt-in toggle.
- Extract a reusable `fetchTopicPreviewImages` view modifier so all three
  screens share the same lazy-fetch + per-session dedupe logic, and allow a
  retry on request failure.
- Backend caches a fixed upper bound (9) of preview URLs so changing the
  displayed count needs no refetch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants