Fix Bug 1402654 - Allow custom image / thumbnail for an edited top site #3891
Conversation
|
@aaronrbenson What's the expected behavior for various interactions and does this current PR look correct? This used to have the page's screenshot, but when the image failed, it switched to just the letter fallback. The change to letter could be useful as an extra indication that it attempted. Also, is the failure message text and sizing correct -- I don't see that message in the invision doc: The preview link should not be interact-able and should have the image cropped differently? (Currently you can click it, and it navigates.) This shows up when deleting the content of the input box: I guess we shouldn't show the old screenshot when the url is cleared? Also, the sizing of the text and the string for the custom image should have ellipsis…? Also, what should cause the custom image input to disappear? Right now moving focus away from the image input box when there's no value makes it disappear. Is this the desired layout with the URL red error message? We probably want a non-text-selection cursor for hovering… and the color should be? The size of the confirmation button changes widths when it goes from Preview to Save, but probably not too much we can do there while not causing oddness for l10n? |
|
The first thing I tried to test was editing a default top site's image, and it kept showing the loading animation forever. I'm guessing it's because the default top site isn't handled in But @k88hudson do you have suggestions on how to better handle all of this state? Also, looks like things don't quite match the invision doc in terms of layout, strings, sizing, behavior, etc.; and some things don't seem quite defined by design either, so followup there too. |
| @@ -146,6 +146,13 @@ topsites_form_add_button=Add | |||
| topsites_form_save_button=Save | |||
| topsites_form_cancel_button=Cancel | |||
| topsites_form_url_validation=Valid URL required | |||
| topsites_form_label_title=Title | |||
| topsites_form_label_url=URL | |||
Mardak
Dec 5, 2017
Member
nit: let's put these next to their related placeholders, so group together topsites_form_title_* and _url_*:
…title_label=…
…title_placeholder=…
nit: let's put these next to their related placeholders, so group together topsites_form_title_* and _url_*:
…title_label=…
…title_placeholder=…| @@ -146,6 +146,13 @@ topsites_form_add_button=Add | |||
| topsites_form_save_button=Save | |||
| topsites_form_cancel_button=Cancel | |||
| topsites_form_url_validation=Valid URL required | |||
| topsites_form_label_title=Title | |||
| topsites_form_label_url=URL | |||
| topsites_form_label_screenshot_url=Custom Image | |||
Mardak
Dec 5, 2017
Member
Probably use topsites_form_image_label or at least not use "screenshot" as that could be confusing for localizers looking at the name of the string as it doesn't seem to relate to the string
Probably use topsites_form_image_label or at least not use "screenshot" as that could be confusing for localizers looking at the name of the string as it doesn't seem to relate to the string
| @@ -95,18 +95,25 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) { | |||
| } | |||
| return Object.assign({}, prevState, {initialized: true, rows: action.data}); | |||
| case at.TOP_SITES_EDIT: | |||
| return Object.assign({}, prevState, {editForm: {visible: true, site: action.data}}); | |||
| return Object.assign({}, prevState, {editForm: {visible: true, site: action.data}}, {screenshotRequestFailed: false}); | |||
Mardak
Dec 5, 2017
Member
Any reason to add a new object here instead of just adding the property to the existing one that has editForm?
Any reason to add a new object here instead of just adding the property to the existing one that has editForm?
piatra
Dec 7, 2017
Author
Collaborator
Changed.
Changed.
| // cached results from previous requests. | ||
| screenshotPreview: null, | ||
| showCustomScreenshotForm: false, | ||
| screenshotRequestFailed: false |
Mardak
Dec 5, 2017
Member
Something about this doesn't seem right with the Reducers.jsm change also tracking some of the same state? Should more of this be in the redux state instead of this local state? Or some other way to track all of this? I suppose one aspect is that only one form for a given site would be open at any time? @k88hudson
Something about this doesn't seem right with the Reducers.jsm change also tracking some of the same state? Should more of this be in the redux state instead of this local state? Or some other way to track all of this? I suppose one aspect is that only one form for a given site would be open at any time? @k88hudson
piatra
Dec 7, 2017
Author
Collaborator
Current implementation adds:
In the TopSiteForm:
- customScreenshotUrl - this is more or less required for the input field to work + to compare with the prop and see if the input value changed
- pendingScreenshotUpdate - to shows the loading indicator
- screenshotPreview - this is a side effect of the
BroadcastToContent (maybe I'm missing something): the user can exit the preview/edit flow by just refreshing the page so we don't have a chance to clear props.Topsite.screenshotPreview set in the Reducer. If you click edit again you will be presented with the previous preview image but the url associated is lost. Having this locally means it automatically gets cleared when the component dismounts.
- topsiteUrlValidationError & screenshotUrlValidationError - This way I can notify the TopSiteEditInputs component of validation errors (to show error labels & focus the elements) while at the same time keep the validation methods in the TopSiteForm component where they are needed more (add/save/preview functionality).
In TopSiteEditInputs
- showCustomScreenshotForm - to force the input to show
Current implementation adds:
In the TopSiteForm:
- customScreenshotUrl - this is more or less required for the input field to work + to compare with the prop and see if the input value changed
- pendingScreenshotUpdate - to shows the loading indicator
- screenshotPreview - this is a side effect of the
BroadcastToContent(maybe I'm missing something): the user can exit the preview/edit flow by just refreshing the page so we don't have a chance to clearprops.Topsite.screenshotPreviewset in the Reducer. If you click edit again you will be presented with the previous preview image but the url associated is lost. Having this locally means it automatically gets cleared when the component dismounts. - topsiteUrlValidationError & screenshotUrlValidationError - This way I can notify the TopSiteEditInputs component of validation errors (to show error labels & focus the elements) while at the same time keep the validation methods in the TopSiteForm component where they are needed more (add/save/preview functionality).
In TopSiteEditInputs
- showCustomScreenshotForm - to force the input to show
| @@ -293,6 +335,9 @@ this.TopSitesFeed = class TopSitesFeed { | |||
| case at.TOP_SITES_UNPIN: | |||
| this.unpin(action); | |||
| break; | |||
| case at.SCREENSHOT_REQUEST: | |||
| this.setCustomTopsiteScreenshot(action.data); | |||
Mardak
Dec 5, 2017
Member
This is called on a preview, so calling a set would seem to be more permanent than a preview. So either the method is not actually permanent or it shouldn't be as permanent?
This is called on a preview, so calling a set would seem to be more permanent than a preview. So either the method is not actually permanent or it shouldn't be as permanent?
piatra
Dec 7, 2017
Author
Collaborator
s/set/get since the function now calls getScreenshotForURL.
s/set/get since the function now calls getScreenshotForURL.
| Screenshots.maybeCacheScreenshot(link, customScreenshotURL, "screenshotPreview", | ||
| // When done, broadcast the result of the request. | ||
| screenshotPreview => { | ||
| this.store.dispatch(ac.BroadcastToContent({ |
Mardak
Dec 5, 2017
Member
We probably don't need to broadcast to all pages when one form on a page is requesting the image? This would also change the main store's state which doesn't seem right for a transient image preview?
We probably don't need to broadcast to all pages when one form on a page is requesting the image? This would also change the main store's state which doesn't seem right for a transient image preview?
piatra
Dec 5, 2017
Author
Collaborator
@Mardak I agree with the comment but I'm not sure how to address this. Any advice on how to pass the message?
@Mardak I agree with the comment but I'm not sure how to address this. Any advice on how to pass the message?
Mardak
Dec 5, 2017
Member
I believe @sarracini was looking at adding something like SendOnlyToContent in https://bugzilla.mozilla.org/show_bug.cgi?id=1415491 but before then, I suppose the closest would be SendToContent, but that would still update main store.
@k88hudson is there a preferred way to send transient data to a single content page without updating main store?
I believe @sarracini was looking at adding something like SendOnlyToContent in https://bugzilla.mozilla.org/show_bug.cgi?id=1415491 but before then, I suppose the closest would be SendToContent, but that would still update main store.
@k88hudson is there a preferred way to send transient data to a single content page without updating main store?
piatra
Dec 7, 2017
Author
Collaborator
the Broadcast has remained the same atm.
the Broadcast has remained the same atm.
| @@ -165,19 +169,22 @@ this.TopSitesFeed = class TopSitesFeed { | |||
| * Get an image for the link preferring tippy top, rich favicon, screenshots. | |||
| */ | |||
| async _fetchIcon(link) { | |||
| const hasCustomScreenshot = !!link.customScreenshotURL; | |||
Mardak
Dec 5, 2017
•
Member
We should probably short circuit first if it has a custom screenshot and do some custom behavior. Or maybe fetchIcon just shouldn't be called if we aren't using an icon anyway?
We should probably short circuit first if it has a custom screenshot and do some custom behavior. Or maybe fetchIcon just shouldn't be called if we aren't using an icon anyway?
piatra
Dec 7, 2017
Author
Collaborator
Now this never gets called if customScreenshotURL is set, instead if the customScreenshot is missing it will trigger a maybeCacheScreenshot.
Now this never gets called if customScreenshotURL is set, instead if the customScreenshot is missing it will trigger a maybeCacheScreenshot.
| */ | ||
| async replaceScreenshot(newScreenshotURL, url) { | ||
| // .copy() will overwrite any existing thumbnail associated with `url` | ||
| await PageThumbsStorage.copy(newScreenshotURL, url); |
Mardak
Dec 5, 2017
Member
This doesn't seem right. There are other consumers of PageThumbs, so having it show some custom image that is not the page's thumbnail will probably lead to bugs. Highlights doesn't replace a url's thumbnail with the preview_image_url by requesting the thumbnail for that image instead of the page. Maybe something like that could be used for these custom preview images for top sites?
This doesn't seem right. There are other consumers of PageThumbs, so having it show some custom image that is not the page's thumbnail will probably lead to bugs. Highlights doesn't replace a url's thumbnail with the preview_image_url by requesting the thumbnail for that image instead of the page. Maybe something like that could be used for these custom preview images for top sites?
piatra
Dec 7, 2017
Author
Collaborator
Changed this. Now a preview request will trigger a getScreenshotForURL and we attach that in the Reducer to the Topsite as screenshotPreview.
If the user saves the form (it pins the site) we save the URL in the pinned pref as customScreenshotURL and upon the feed refresh we trigger maybeCacheScreenshot which adds customScreenshot to the LinksCache object that represents that Topsite. The customScreenshot is also migrated between links so we need to clear the value if the user updated the customScreenshotURL for the website.
Changed this. Now a preview request will trigger a getScreenshotForURL and we attach that in the Reducer to the Topsite as screenshotPreview.
If the user saves the form (it pins the site) we save the URL in the pinned pref as customScreenshotURL and upon the feed refresh we trigger maybeCacheScreenshot which adds customScreenshot to the LinksCache object that represents that Topsite. The customScreenshot is also migrated between links so we need to clear the value if the user updated the customScreenshotURL for the website.
| }; | ||
| this.onLabelChange = this.onLabelChange.bind(this); | ||
| this.onUrlChange = this.onUrlChange.bind(this); | ||
| this.onCancelButtonClick = this.onCancelButtonClick.bind(this); | ||
| this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(this); | ||
| this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this); | ||
| this.onHideCustomScreenshotForm = this.onHideCustomScreenshotForm.bind(this); |
Mardak
Dec 5, 2017
Member
With the 3 of these label/inputs, it would seem natural to move to a component
With the 3 of these label/inputs, it would seem natural to move to a component
Mardak
Dec 5, 2017
Member
The 3 here I was referring to the 3 label/inputs: title label/input, url label/input, image label/input
The 3 here I was referring to the 3 label/inputs: title label/input, url label/input, image label/input
piatra
Dec 7, 2017
Author
Collaborator
Created TopSiteEditInputs.jsx
- includes the url, title and custom url inputs
- handles the logic to display the error labels and to focus the inputs
- toggle to show hide the custom screenshot input
- All the validation and the input change handler are still in the TopsiteForm.
Created TopSiteEditInputs.jsx
- includes the url, title and custom url inputs
- handles the logic to display the error labels and to focus the inputs
- toggle to show hide the custom screenshot input
- All the validation and the input change handler are still in the TopsiteForm.
| imageClassName = `screenshot${link.screenshot ? " active" : ""}`; | ||
| imageStyle = {backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none"}; | ||
| // If the screenshot request failed use letter fallback. | ||
| if (!screenshotRequestFailed) { |
Mardak
Dec 5, 2017
Member
Something about this seems unnecessarily complex,but I'll need to think about it a bit more. I suppose we should be able to avoid all the lines of change by having a empty
if (screenshotRequestFailed) {
// Just use the letter fallback
} else if (screenshotPreview…) {
} else if (tippyTopIcon …) {
Something about this seems unnecessarily complex,but I'll need to think about it a bit more. I suppose we should be able to avoid all the lines of change by having a empty
if (screenshotRequestFailed) {
// Just use the letter fallback
} else if (screenshotPreview…) {
} else if (tippyTopIcon …) {
piatra
Dec 7, 2017
Author
Collaborator
Changed as suggested.
Changed as suggested.
|
Could I get a build to do some UX quality checking for this? There are a lot of little interactions and behaviors that @Mardak raises above and it would be helpful if I could test it out locally to provide notes. |
| topsites_form_label_screenshot_url=Custom Image | ||
| topsites_form_screenshot_request_error=Image failed to load. Try a different URL. | ||
| topsites_form_activate_custom_screenshot=Use a custom image | ||
| topsites_form_custom_screenshot_placeholder=Paste a URL here |
pdehaan
Dec 5, 2017
Collaborator
Why does one placeholder say "Type or paste" and the other is just "Paste"? Can't you type into the topsites URL?
topsites_form_url_placeholder=Type or paste a URL
topsites_form_custom_screenshot_placeholder=Paste a URL here
Why does one placeholder say "Type or paste" and the other is just "Paste"? Can't you type into the topsites URL?
topsites_form_url_placeholder=Type or paste a URL
topsites_form_custom_screenshot_placeholder=Paste a URL here
piatra
Dec 8, 2017
Author
Collaborator
I'll bring this up for discussion.
I'll bring this up for discussion.
2a7bd74
to
d16c19f
|
@piatra could you comment on each review comment on what changed? It's hard to track down what's going on with so many commits with it already being a large change. Also, it would have been helpful to have a summary of what the approach is in the initial PR message. Did you get UX feedback? And if so, what are the decisions? |
|
@Mardak will do. |
| @@ -12,6 +12,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs", | |||
| "resource://gre/modules/BackgroundPageThumbs.jsm"); | |||
| XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", | |||
| "resource://gre/modules/PageThumbs.jsm"); | |||
| XPCOMUtils.defineLazyModuleGetter(this, "PageThumbsStorage", | |||
| "resource://gre/modules/PageThumbs.jsm"); | |||
sarracini
Dec 14, 2017
Contributor
where are you using PageThumbsStorage?
where are you using PageThumbsStorage?
piatra
Dec 14, 2017
Author
Collaborator
@sarracini the implementation changed a few times around and I forgot to remove it. Done. Thanks.
@sarracini the implementation changed a few times around and I forgot to remove it. Done. Thanks.
2e892e4
to
fbbcce1
|
Are there updated designs or a description of how things should be changed? |
|
There is no one source of information but I will try to list everything.
Desired but not implemented:
No required:
Let me know if I skipped something. |
fbbcce1
to
5063658
|
I've rebased the patch against the latest drag and drop work. Waiting on review. I still think there is a bug in the LinksCache causing an issue with this PR, I've added a test (the one failing) that tries to capture the behavior. activity-stream/system-addon/lib/LinksCache.jsm Lines 109 to 111 in 84fd287 I think the problem is that updateLink always references the first newLink object created (even after the property is migrated) so calling expire and then updateLink will not update the newest object.
@Mardak / @k88hudson can you help validate this? What I’ve done is tag every link object with an incrementing id and then track who is being updated in the In this video https://ed.agadak.net/as/activity-stream.edit-preview.webm you do not see the bug because the sequence of events is:
When you add a new Topsite Here is another video actually showing it fail when we first pin a site (to trigger |
Ah indeed. Looks like we want to keep the original |
|
Should this PR fix it or have it done separately? |
5063658
to
8ff3df9
|
Still working through this PR as there's 39 commits and various changes that could have been separated. Ideally it should have been broken up into some commits that would have been individually easier to review, e.g.:
|
07420ab
to
f104567
|
This will need to be rebased on top of #3965. |
* TopSites reducer clears screenshotPreview and requestFailed status when necessary * Added action for content process to update its own reducer when a request is canceled
8f62da6
to
7b4ab9d
|
I'm not sure if d7d40fa is what you meant in this comment I didn't understand the reference to TOP_SITES_EDIT
There might be a weird edge case here (I'm still thinking about it):
So what I did was remove Still waiting to hear on the decision about reverting from an error state |
This comment has been minimized.
This comment has been minimized.
Mardak
commented on system-addon/content-src/components/TopSites/TopSiteForm.jsx in d7d40fa
Mar 21, 2018
•
|
What it was before should have been fine, i.e., AlsoToMain(PREVIEW_REQUEST). Just the one PREVIEW_REQUEST that both content and main handle. Content would have had a previous TOP_SITES_EDIT allowing Reducers to update editForm while main would skip changing state (but TopSitesFeed would request a screenshot). |
This reverts commit d7d40fa.
|
This should be good with some final nits! Thanks! |
| function OnlyToThisContent(action) { | ||
| return _RouteMessage(action, { | ||
| from: CONTENT_MESSAGE_TYPE, | ||
| to: CONTENT_MESSAGE_TYPE |
Mardak
Mar 22, 2018
Member
Did this OnlyToThisContent get added back? I thought it was removed
Did this OnlyToThisContent get added back? I thought it was removed
| editForm: { | ||
| index: prevState.editForm.index, | ||
| previewResponse: action.data.preview, | ||
| previewUrl: action.data.url |
Mardak
Mar 22, 2018
Member
Shouldn't need to re-assign the values for index and previewUrl as they're the same as prevState's. Although we end up with nested Object.assigns.. not sure if that means we should be making the state flatter? @k88hudson ?
return Object.assign({}, prevState, {editForm:
Object.assign({}, prevState.editForm, {previewResponse: action.data.preview})});
Shouldn't need to re-assign the values for index and previewUrl as they're the same as prevState's. Although we end up with nested Object.assigns.. not sure if that means we should be making the state flatter? @k88hudson ?
return Object.assign({}, prevState, {editForm:
Object.assign({}, prevState.editForm, {previewResponse: action.data.preview})});
piatra
Mar 22, 2018
Author
Collaborator
Had that at first but makes it more difficult to read. I also think it helps in this case to be explicit about what we want our props to be (for debugging/refactoring reasons).
Had that at first but makes it more difficult to read. I also think it helps in this case to be explicit about what we want our props to be (for debugging/refactoring reasons).
| case at.TOP_SITES_CANCEL_EDIT: | ||
| return Object.assign({}, prevState, {editForm: null}); | ||
| case at.PREVIEW_RESPONSE: | ||
| if (prevState.editForm && | ||
| prevState.editForm.previewUrl === action.data.url) { |
Mardak
Mar 22, 2018
Member
nit: The other actions short circuit to return prevState, so let's make this follow that pattern too.
if (!prevState.editForm || action.data.url !== prevState.editForm.previewUrl) {
return prevState;
}
nit: The other actions short circuit to return prevState, so let's make this follow that pattern too.
if (!prevState.editForm || action.data.url !== prevState.editForm.previewUrl) {
return prevState;
}| if (defaultStyle) { // Render the Topsite without any image or icon | ||
| smallFaviconFallback = false; | ||
| } else if (link.customScreenshotURL) { | ||
| // styles and class names for top sites with custom screenshot |
Mardak
Mar 22, 2018
Member
This comment doesn't really explain why we're using ".rich-icon" for custom screenshots
This comment doesn't really explain why we're using ".rich-icon" for custom screenshots
| @@ -67,7 +67,16 @@ export class TopSiteLink extends React.PureComponent { | |||
| let showSmallFavicon = false; | |||
| let smallFaviconStyle; | |||
| let smallFaviconFallback; | |||
| if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) { | |||
| if (defaultStyle) { // Render the Topsite without any image or icon | |||
Mardak
Mar 22, 2018
Member
This comment style doesn't match the rest. Would be good to hint at "even if the link has imagery"
This comment style doesn't match the rest. Would be good to hint at "even if the link has imagery"
|
|
||
| _updateCustomScreenshotInput(value) { | ||
| this.setState({ | ||
| customScreenshotUrl: value, |
Mardak
Mar 22, 2018
Member
nit: if you name the parameter as customScreenshotUrl, then you can omit the : value
nit: if you name the parameter as customScreenshotUrl, then you can omit the : value
| customScreenshotUrl: value, | ||
| validationError: false | ||
| }); | ||
| this.props.dispatch(ac.OnlyToThisContent({type: at.PREVIEW_REQUEST_CANCEL})); |
| const changed = customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous; | ||
| // Preview mode if changes were made to the custom screenshot URL and no preview was received yet | ||
| // or the request failed | ||
| const previewMode = (changed && this.props.previewResponse === null) || requestFailed; |
Mardak
Mar 22, 2018
Member
This should be able to be just previewMode = changed && !this.props.previewResponse If the value wasn't changed, we shouldn't be looking for a preview request.
This should be able to be just previewMode = changed && !this.props.previewResponse If the value wasn't changed, we shouldn't be looking for a preview request.
Mardak
Mar 22, 2018
Member
(The input change dispatching PREVIEW_REQUEST_CANCEL causes requestFailed to get cleared.)
(The input change dispatching PREVIEW_REQUEST_CANCEL causes requestFailed to get cleared.)







Closes https://bugzilla.mozilla.org/show_bug.cgi?id=1402654
How it works
SCREENSHOT_REQUESTthat gets handled in the TopSitesFeedgetScreenshotPreview. this makes aScreemshot.getScreenshotForURLrequest and dispatches a SCREENSHOT_UPDATE with the response (or SCREENSHOT_FAILED).screenshotPrevieworcustomScreenshotin addition toscreenshotto a Topsite.screenshotPreview || customScreenshotUI/UX
There is no one source of information but I will try to list everything.
Latest design https://mozilla.invisionapp.com/share/7DEP1VMYX#/screens/266461200_Top_Site_-_Edit_Image_
In addition to that:
Desired but not implemented:
No required:
Let me know if I skipped something.