Skip to content
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

✨ [bento][amp-date-countdown] Initial Preact component for amp-date-countdown #30092

Merged
merged 12 commits into from Sep 14, 2020

Conversation

krdwan
Copy link
Contributor

@krdwan krdwan commented Sep 2, 2020

Tracking Issue: #30052
Design Doc: Doc Link

Initial amp-date-countdown preact component. Preact component takes in user defined props to calculate a date to countdown to. This is labeled epoch. Then we use setInterval to continuously calculate the difference between the current time and the epoch time (this is labeled timeLeft).

Please see Design Doc for more details on the component.

Design is similar to DateDisplay Bento component.

@google-cla google-cla bot added the cla: yes label Sep 2, 2020
@@ -59,3 +59,11 @@ export function WithAmpContext(props) {

return <AmpContext.Provider children={props['children']} value={current} />;
}

/**
*
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might need some help here with typing.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is for now a JsonObject. Not for much longer I think, but still the case. I'll ping you when this changes if you haven't yet merged this pull request.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here it is: #30102

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks Dima sounds good!

@krdwan krdwan marked this pull request as ready for review September 2, 2020 16:16
if (input < -9 || input > 9) {
return String(input);
} else if (input >= -9 && input < 0) {
return '-0' + Math.abs(input);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit:

Suggested change
return '-0' + Math.abs(input);
return '-0' + (-input);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good updated!

/**
* @param {?string} message
*/
function throwWarning(message) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Let's rename this, since it doesn't actually throw.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good updated to displayWarning


useEffect(() => {
const interval = setInterval(() => {
const newTimeLeft = new Date(epoch) - new Date();
Copy link
Contributor

Choose a reason for hiding this comment

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

new Date(epoch) is invariant to the interval loop, so let's move it out of setInterval callback. Also if you switch to using Date.now() you can avoid extra allocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, moved new Date(epoch) out of the interval loop and changed epoch variable to be it's Date formatted version.

Also switched to using Date.now()

useEffect(() => {
const interval = setInterval(() => {
const newTimeLeft = new Date(epoch) - new Date();
setTimeLeft(() => newTimeLeft);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why callback setter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did not realize the appropriate syntax for this. Have updated.

const [timeLeft, setTimeLeft] = useState(new Date(epoch) - new Date());

useEffect(() => {
const interval = setInterval(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have to call setInterval and clearInterval from an actual window here. Unlike a one-off setTimeout or open, the risk of leaking from setInterval is very real. So, please add a ref to the root element and use it to resolve window (win = element.ownerDocument.defautlView) and use that window to call set/clearInterval.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, updated!

return () => clearInterval(interval);
}, [playable]);

const data = Object.assign(
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's do {...getYDMDMDJMD(), ...getLocaleWord()}.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good updated!

* @return {Object}
*/
function getYDHMSFromMs(ms, biggestUnit) {
/** @enum {number} */
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not a enum since this map is accessed by a variable key. This is Object<string, number>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated in the hoisted const.

* days, hours, minutes, etc. and returns formatted strings in an object.
* @param {number} ms
* @param {string} biggestUnit
* @return {Object}
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a JsonObject as a result.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated!

Copy link
Contributor

Choose a reason for hiding this comment

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

Not seeing this change. Did you push?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I missed it, it should be included in the latest revision. Thanks!


/**
* @typedef {{
* endDate (string|undefined),
Copy link
Contributor

Choose a reason for hiding this comment

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

colons missing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated!

@@ -59,3 +59,11 @@ export function WithAmpContext(props) {

return <AmpContext.Provider children={props['children']} value={current} />;
}

/**
*
Copy link
Contributor

Choose a reason for hiding this comment

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

This is for now a JsonObject. Not for much longer I think, but still the case. I'll ping you when this changes if you haven't yet merged this pull request.

@krdwan
Copy link
Contributor Author

krdwan commented Sep 3, 2020

Thanks for the review @dvoytenko @jridgewell . New revision is up with changes addressed. Key highlights:

  • Use useRef to explicitly resolve window
  • Countdown will not be initiated (or will stop if already initiated) if AMP context -> playable is false
  • Moved locale strings into a new config file
  • Use useMemo to remove repetitive calculations on every interval

May have a few more updates / merge conflict resolutions after #30102 is merged.

@@ -0,0 +1,52 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's rename this file to just messages.js.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good updated!

* Strings representing years, minutes, etc. in various locales
* @typedef {Array<string>}
*/
let DateCountdownLocaleDef;
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for this type def. We just should use !Array<string> inline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated!

/**
* @return {JsonObject}
*/
export function useAmpContext() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rebase. This should no longer be needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks updated!

...rest
}) {
useResourcesNotify();
const {'playable': playable} = useAmpContext();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should no longer need quotes. Rebase and go back to const {playable} = useAmpContext()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated!

* days, hours, minutes, etc. and returns formatted strings in an object.
* @param {number} ms
* @param {string} biggestUnit
* @return {Object}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not seeing this change. Did you push?

}
const win = rootRef.current.ownerDocument.defaultView;
const interval = win.setInterval(() => {
const newTimeLeft = epoch - Date.now() + DELAY;
Copy link
Member

Choose a reason for hiding this comment

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

what is the + DELAY for?

Copy link
Contributor Author

@krdwan krdwan Sep 8, 2020

Choose a reason for hiding this comment

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

Since the setInterval's callback is not being called until 1000ms (1 delay) from now, setting the correct time to be displayed at that time. So it will start with the initial value being displayed, and calculate the new time to be 1000ms less than the initial value, and will display that in 1000ms.

const rootRef = useRef(null);

useEffect(() => {
if (!playable) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we also early-return if the rootRef isn't set yet? e.g.:

if (!playable || ! rootRef.current) { return }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated!

epoch = timestampSeconds * 1000;
}

if (epoch === undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

What is the expected behavior when multiple are provided? e.g. both endDate and timeleftMs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the current 0.1 implementation, it checks for the first one that is available in the order endDate, timeleftMs, timestampMs, and timestampeSeconds similar to how it's done here.

@krdwan
Copy link
Contributor Author

krdwan commented Sep 8, 2020

Thanks for reviews @dvoytenko @samouri !

Latest revision should have all requested changes!

() =>
new Date(
getEpoch(endDate, timeleftMs, timestampMs, timestampSeconds) +
offsetSeconds * DELAY
Copy link
Member

Choose a reason for hiding this comment

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

Probably should use MILLISECONDS_IN_SECOND here instead of DELAY right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated!

extensions/amp-date-countdown/1.0/date-countdown.js Outdated Show resolved Hide resolved
* Strings representing years, minutes, etc. in various locales
* @type {Object<string, !Array<string>>}
*/
const LOCALE_WORD = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't we just store this in the expected { years: … } format? We could then avoid the useMemo and everything else necessary.

Copy link
Contributor Author

@krdwan krdwan Sep 9, 2020

Choose a reason for hiding this comment

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

So the idea would be to do something like LOCALE_WORD[props['locale']] at each iteration?

One thing I am concerned about is there is a helper function that checks that the props['locale'] value is valid and displays a warning if not. The warning really should only be displayed one time. So even if we were to change the format, we would still have to useMemo to store some info about what our locale is or that the warning message has already been displayed.

Happy to update the format, if there's something I'm missing. If I am misunderstanding please let me know!

() =>
new Date(
getEpoch(endDate, timeleftMs, timestampMs, timestampSeconds) +
offsetSeconds * DELAY
Copy link
Contributor

Choose a reason for hiding this comment

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

Yup.

Copy link
Contributor

@dvoytenko dvoytenko left a comment

Choose a reason for hiding this comment

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

LG on my side. 1 question. Please complete the review with others.

extensions/amp-date-countdown/1.0/date-countdown.js Outdated Show resolved Hide resolved
@krdwan
Copy link
Contributor Author

krdwan commented Sep 9, 2020

Thanks for reviews @dvoytenko @jridgewell @samouri !

Latest revision is up with responses to comments!

@dvoytenko
Copy link
Contributor

LG from me. But please wait for another LG.

@krdwan krdwan merged commit ec0f14c into ampproject:master Sep 14, 2020
ed-bird pushed a commit to ed-bird/amphtml that referenced this pull request Dec 10, 2020
…ountdown (ampproject#30092)

* First commit Preact component

* Add clear comments for helper functions

* Various review comment updates

* More review updates

* More review updates

* Add useMemo

* Updates for code review

* Review comments and rebase

* Remove rebase conflict

* remove quotes on playable

* Remove date format from epoch

* add quotes for object keys that are output to template
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants