-
Notifications
You must be signed in to change notification settings - Fork 29
When the user clicks a thumbnail image, show the full-size image in an image viewer widget #341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There is no close button for Image previewer. Only the button to close the entire program. |
|
robrix/src/image_viewer_modal.rs Line 15 in cd9c6a4
Line 210 in cd9c6a4
We can keep this pr open and I shall put my energies elsewhere issues, IMHO |
Right, but in general, the Matrix SDK can provide image metadata including dimensions, so you can use that as a placeholder until you download the entire image. Even if you don't know the size of the image, you can just show the thumbnail image in the ImageViewer widget until the full-size image has been fully-fetched, and then you will know the size of the full image. Regardless, it should not matter what the image's size is. The ImageViewer should be full-screen (
You're correct, the Modal widget doesn't. We can modify it, but in general, the Modal widget has lots of issues right now. Don't use the Modal widget; you can copy my
I don't understand what this means, sorry.
Yes, of course, you'll have to do it on a background thread/task. We already have the infrastructure for this. In summary, it sounds like you're a bit stuck on this, which is totally fine. Thanks for submitting your work thus far; in the future, I (or others) can help you complete the ImageViewer widget. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the redesign effort.
I need more info in order to determine whether this is the correct approach, because right now I feel that your latest MediaCache redesign is too complicated again.
Recently, you said that a thumbnail version of an image may have a different MxcUri
than the URI of the full-size image. That makes sense, but i have some questions:
- Are the URIs ever the same? If so, what are the cases where this happens?
- How do we know if two URIs point to two different versions of the same image? Can we know this? If so, where in the code is this known?
- Do we learn about the URI for a full-size image separately from the URI for a thumbnail, or are they both known together?
This is your new redesign:
BTreeMap<OwnedMxcUri, (Option<OwnedMxcUri>, MediaCacheEntryRef, MediaCacheEntryRef)>
a few questions about this design:
- Is the key of the BTreeMap the URI for the full image, or for a thumbnail? or can it be either?
- What is the optional URI in the first part of the tuple?
- Why is that part of the value itself? If you keep this in the value part of the map, you can't search for it, so that seems fairly pointless.
- Why are there still two
MediaCacheEntryRef
s within the map value?
Never same, always different
Note Lines 61 to 76 in 90e1910
I afraid i dont know the meaning of I have to say, we actually store 3 different clarity level:
Both, as the same as you said either.
We do 1 or 2 queries: First query, we will get the Some(MxcUri):For timeline => We use this mxcuri as the key do the second query, then get or fetch For image viewer => get or fetch None:2 case here:
we need to rely on if else, this condition must for image viewer, return the medium value of the tuple, maybe thumbnail uri, maybe original uri, So we return _ & File, it is in the medium value of the tuple. (if it has not been fetched, we return the last value of the tuple temporarily(it is _ & 400x400), when the _ & File was fetched, it could autoly replace it.) |
The thumbnail uri, we know it is an Also, if
Because there are possible secondary queries here.
The middle value exists to deal with a rare case, where the middle value is rarely used. |
We should not use |
ok i think this is what I missed. What does "original" mean here? I presumed we were only going to store two possible sizes: the full-size file, and a 400x400 thumbnail. |
// We need to distinct these two similar actions because a potential bug. | ||
// if we combine these 2 actions into 1, will appear a rare bug-condition: | ||
// | ||
// User click some image and **quickly close** the image viewer, | ||
// firstly, the `_ & File` version image has not been fetched, `try_get_image_or_fetch` would return the matching thumbnail to as temporariness, | ||
// Once the `_ & File` version image is beening fetched, it would autoly open image viewer and show the image, | ||
// but image viewer was already closed by user. | ||
// That is, image viewer will show up for no reason in users' view. | ||
// feel free to modify comments. | ||
Show(Arc<[u8]>), | ||
MakeItClearer(Arc<[u8]>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, so the difference between these two actions is:
Show
will always show the image viewer, even if it is not currently shown.MakeItClearer
will reload the image displayed within the ImageViewer, but will not show the ImageViewer if it is not already shown.
Makes sense. I would rename MakeItClearer
to be less vague... perhaps something like "ReplaceImage" since that's the only thing that should be done upon that action occurring.
Then in the docs for each action, you can describe what that action does.
// We only handle events if the status is `Image`. | ||
if TextOrImageStatus::Image != self.status() { return }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, you cannot do this here. You still need to forward all events to the underlying self.view
(by calling self.view.handle_event()
), so don't skip that.
What you actually want to do here is to only call the event.hits()
function if an image is being shown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved in #443
impl TextOrImage { | ||
/// Sets the text content, which will be displayed on future draw operations. | ||
/// | ||
/// ## Arguments | ||
/// * `text`: the text that will be displayed in this `TextOrImage`, e.g., | ||
/// a message like "Loading..." or an error message. | ||
pub fn show_text<T: AsRef<str>>(&mut self, cx: &mut Cx, text: T) { | ||
fn show_text<T: AsRef<str>>(&mut self, cx: &mut Cx, text: T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did you make these private? please restore them as public since they are public APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impl TextOrImageRef
should be public.
So impl TextOrImage
no need to be public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, not quite true. You can't always get a widgetref in the code, so these functions should be public too.
If a widgetref function is public, the inner function implemented on that widget should also be public.
if let MediaCacheEntry::Loaded(data) = tl.media_cache.try_get_media_or_fetch(mxc_uri, false, image_viewer_insert_into_cache) { | ||
Cx::post_action(ImageViewerAction::Show(data)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo, this offers a poor user experience. What happens if the user clicks an image but this if statement is not true? Nothing would happen, which would confuse the user.
If that cannot possibly happen (which I think is true here), then you should redesign the TextOrImageAction::Click
type to avoid this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// `unwrap` is definitely safe here because we have set keys already. | ||
let (thumbnail_uri, better_entry, entry) = self.cache.get(mxc_uri).unwrap().clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
considering that this is a public function, you are incorrect here -- you cannot assume that the caller has explicitly called set_keys()
before this. Even if you could assume that, you should not.
I have left a few comments but have not done a formal review. To be honest, I'm having difficulty understanding some of your answers and also the code in the In my mind, a media cache (or really, any kind of cache) just stores some data that is referenced by a key. It seems like we agree that a I still don't see why we need such complexity in the cache with 4 different cases that are handled so many different ways. The code is just very very hard to read/understand. Given that I will be busy for the next 10-11 days with conference travel, I would recommend that your next step be to meet with Alex or your fellow interns and present your design to them to see if they can help you think of a better, simpler design. Then feel free to report back here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's too much code changes. There is no need to change the text_or_image widget and the function "try_get_media_or_fetch". You can refer to how OpenMessageContextMenu works by using the item id without storing any message content inside MessageDetails. You should create something like "OpenImageContextViewer".
if let MediaCacheEntry::Loaded(data) = tl.media_cache.try_get_media_or_fetch(mxc_uri, false, image_viewer_insert_into_cache) { | ||
Cx::post_action(ImageViewerAction::Show(data)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if self.view.button(id!(close_button)).clicked(actions) { | ||
self.close(cx); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow key press event Esc to close the image viewer
let MediaSource::Plain(original_mxc_uri) = original_source else { return }; | ||
|
||
let mxc_uri_for_timeline = thumbnail_mxc_uri.clone().unwrap_or(original_mxc_uri.clone()); | ||
|
||
media_cache.set_keys(original_mxc_uri, thumbnail_mxc_uri); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduce the number of clone here.
`let mxc_uri_for_timeline = thumbnail_mxc_uri.as_ref().unwrap_or(&original_mxc_uri);
media_cache.set_keys(&original_mxc_uri, thumbnail_mxc_uri.as_ref());`
pub fn set_keys(&mut self, original_uri: &OwnedMxcUri, thumbnail_uri: Option<&OwnedMxcUri>) { match thumbnail_uri { Some(uri) => { if let Entry::Vacant(v) = self.cache.entry(uri.clone()) { v.insert((None, Arc::new(Mutex::new(MediaCacheEntry::Null)), Arc::new(Mutex::new(MediaCacheEntry::default())))); } if let Entry::Vacant(v) = self.cache.entry(original_uri.clone()) { v.insert((thumbnail_uri.cloned(), Arc::new(Mutex::new(MediaCacheEntry::Null)), Arc::new(Mutex::new(MediaCacheEntry::default())))); } } None => { if let Entry::Vacant(v) = self.cache.entry(original_uri.clone()) { v.insert((None, Arc::new(Mutex::new(MediaCacheEntry::default())), Arc::new(Mutex::new(MediaCacheEntry::default())))); } } } }
impl Deref for MediaCache { | ||
type Target = BTreeMap<OwnedMxcUri, MediaCacheValue>; | ||
type Target = BTreeMap<OwnedMxcUri, (Option<OwnedMxcUri>, MediaCacheEntryRef, MediaCacheEntryRef)>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why is there a better_entry?
BtreeMap's value should not be a tuple.
&mut self, | ||
mxc_uri: OwnedMxcUri, | ||
requested_format: MediaFormat, | ||
) -> (MediaCacheEntry, MediaFormat) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary removal of Return type MediaFormat
}, | ||
&mut self, | ||
mxc_uri: &OwnedMxcUri, | ||
prefer_thumbnail: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need to add a new parameter prefer_thumbnail. You can obtain the item_id of tl_state.items when clicking the image, find the mxc_uri from the tl_state and then fetch the full image in the image viewer. That way, you wouldn't need to save the mxc_uri into text_or_image widget.
issue 327: When the user clicks a thumbnail image, show the full-size image in an image viewer widget