feat: show image preview in topic list#386
Open
MufanQiu wants to merge 4 commits into
Open
Conversation
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.
Contributor
There was a problem hiding this comment.
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_imagesasync request/response and aTopic.preview_image_urlsfield. - Implement Rust
get_topic_preview_imagesservice 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 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 on lines
+356
to
+359
| .onChange(of: dataSource.latestResponse) { | ||
| updateForumMeta($1) | ||
| fetchPreviewImagesIfNeeded($1) | ||
| } |
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 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() | ||
| }); | ||
| } | ||
| } | ||
|
|
- 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.
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
topic_preview_images(inlogic/service/src/topic.rs) fetches a topic's first post and extracts imageURLs 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, andTopicgainsa
preview_image_urlsfield inDataModel.proto.fetchTopicPreviewImagesview modifier lazilyfetches 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.
(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
Testing
cargo fmt --check,cargo clippy --workspace --all-targets --all-features -- -D warningspass.appear only when enabled, rows without images are unaffected, tapping a row
(including the image strip) opens the topic, and count/height update live.