<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[49146] trunk: Community Events: Display dates and times in the user's time zone.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/49146">49146</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/49146","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2020-10-14 18:19:43 +0000 (Wed, 14 Oct 2020)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Community Events: Display dates and times in the user's time zone.

Fixes <a href="https://core.trac.wordpress.org/ticket/51130">#51130</a>
Props sippis, hlashbrooke, audrasjb, Rarst, iandunn</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcjs_enqueueswpdashboardjs">trunk/src/js/_enqueues/wp/dashboard.js</a></li>
<li><a href="#trunksrcwpadminincludesclasswpcommunityeventsphp">trunk/src/wp-admin/includes/class-wp-community-events.php</a></li>
<li><a href="#trunksrcwpadminincludesdashboardphp">trunk/src/wp-admin/includes/dashboard.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunktestsqunitindexhtml">trunk/tests/qunit/index.html</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsqunitwpadminjsdashboardjs">trunk/tests/qunit/wp-admin/js/dashboard.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcjs_enqueueswpdashboardjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/js/_enqueues/wp/dashboard.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/js/_enqueues/wp/dashboard.js    2020-10-14 18:17:55 UTC (rev 49145)
+++ trunk/src/js/_enqueues/wp/dashboard.js      2020-10-14 18:19:43 UTC (rev 49146)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -266,6 +266,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">        'use strict';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        var communityEventsData = window.communityEventsData || {},
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                dateI18n = wp.date.dateI18n,
+               format = wp.date.format,
+               sprintf = wp.i18n.sprintf,
+               __ = wp.i18n.__,
+               _x = wp.i18n._x,
</ins><span class="cx" style="display: block; padding: 0 10px">                 app;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -441,6 +446,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                .fail( function() {
</span><span class="cx" style="display: block; padding: 0 10px">                                        app.renderEventsTemplate({
</span><span class="cx" style="display: block; padding: 0 10px">                                                'location' : false,
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                'events'   : [],
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 'error'    : true
</span><span class="cx" style="display: block; padding: 0 10px">                                        }, initiatedBy );
</span><span class="cx" style="display: block; padding: 0 10px">                                });
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -465,6 +471,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $locationMessage = $( '#community-events-location-message' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                $results         = $( '.community-events-results' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        templateParams.events = app.populateDynamicEventFields(
+                               templateParams.events,
+                               communityEventsData.time_format
+                       );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         /*
</span><span class="cx" style="display: block; padding: 0 10px">                         * Hide all toggleable elements by default, to keep the logic simple.
</span><span class="cx" style="display: block; padding: 0 10px">                         * Otherwise, each block below would have to turn hide everything that
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -576,6 +587,195 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                app.toggleLocationForm( 'show' );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                },
+
+               /**
+                * Populate event fields that have to be calculated on the fly.
+                *
+                * These can't be stored in the database, because they're dependent on
+                * the user's current time zone, locale, etc.
+                *
+                * @since 5.6.0
+                *
+                * @param {Array}  rawEvents  The events that should have dynamic fields added to them.
+                * @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`.
+                *
+                * @returns {Array}
+                */
+               populateDynamicEventFields: function( rawEvents, timeFormat ) {
+                       // Clone the parameter to avoid mutating it, so that this can remain a pure function.
+                       var populatedEvents = JSON.parse( JSON.stringify( rawEvents ) );
+
+                       $.each( populatedEvents, function( index, event ) {
+                               var timeZone = app.getTimeZone( event.start_unix_timestamp * 1000 );
+
+                               event.user_formatted_date = app.getFormattedDate(
+                                       event.start_unix_timestamp * 1000,
+                                       event.end_unix_timestamp * 1000,
+                                       timeZone
+                               );
+
+                               event.user_formatted_time = dateI18n(
+                                       timeFormat,
+                                       event.start_unix_timestamp * 1000,
+                                       timeZone
+                               );
+
+                               event.timeZoneAbbreviation = app.getTimeZoneAbbreviation( event.start_unix_timestamp * 1000 );
+                       } );
+
+                       return populatedEvents;
+               },
+
+               /**
+                * Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`.
+                *
+                * @since 5.6.0
+                *
+                * @param startTimestamp
+                *
+                * @returns {string|number}
+                */
+               getTimeZone: function( startTimestamp ) {
+                       /*
+                        * Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This
+                        * doesn't need to take `startTimestamp` into account for that reason.
+                        */
+                       var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+                       /*
+                        * Fall back to an offset for IE11, which declares the property but doesn't assign a value.
+                        */
+                       if ( 'undefined' === typeof timeZone ) {
+                               /*
+                                * It's important to use the _event_ time, not the _current_
+                                * time, so that daylight savings time is accounted for.
+                                */
+                               timeZone = app.getFlippedTimeZoneOffset( startTimestamp );
+                       }
+
+                       return timeZone;
+               },
+
+               /**
+                * Get intuitive time zone offset.
+                *
+                * `Data.prototype.getTimezoneOffset()` returns a positive value for time zones
+                * that are _behind_ UTC, and a _negative_ value for ones that are ahead.
+                *
+                * See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off.
+                *
+                * @since 5.6.0
+                *
+                * @param {number} startTimestamp
+                *
+                * @returns {number}
+                */
+               getFlippedTimeZoneOffset: function( startTimestamp ) {
+                       return new Date( startTimestamp ).getTimezoneOffset() * -1;
+               },
+
+               /**
+                * Get a short time zone name, like `PST`.
+                *
+                * @since 5.6.0
+                *
+                * @param {number} startTimestamp
+                *
+                * @returns {string}
+                */
+               getTimeZoneAbbreviation: function( startTimestamp ) {
+                       var timeZoneAbbreviation,
+                               eventDateTime = new Date( startTimestamp );
+
+                       /*
+                        * Leaving the `locales` argument undefined is important, so that the browser
+                        * displays the abbreviation that's most appropriate for the current locale. For
+                        * some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`.
+                        *
+                        * This doesn't need to take `startTimestamp` into account, because a name like
+                        * `America/Chicago` automatically tracks daylight savings.
+                        */
+                       var shortTimeStringParts = eventDateTime.toLocaleTimeString( undefined, { timeZoneName : 'short' } ).split( ' ' );
+
+                       if ( 3 === shortTimeStringParts.length ) {
+                               timeZoneAbbreviation = shortTimeStringParts[2];
+                       }
+
+                       if ( 'undefined' === typeof timeZoneAbbreviation ) {
+                               /*
+                                * It's important to use the _event_ time, not the _current_
+                                * time, so that daylight savings time is accounted for.
+                                */
+                               var timeZoneOffset = app.getFlippedTimeZoneOffset( startTimestamp ),
+                                       sign = -1 === Math.sign( timeZoneOffset ) ? '' : '+';
+
+                               // translators: Used as part of a string like `GMT+5` in the Events Widget.
+                               timeZoneAbbreviation = _x( 'GMT', 'Events widget offset prefix' ) + sign + ( timeZoneOffset / 60 );
+                       }
+
+                       return timeZoneAbbreviation;
+               },
+
+               /**
+                * Format a start/end date in the user's local time zone and locale.
+                *
+                * @since 5.6.0
+                *
+                * @param {int}    startDate   The Unix timestamp in milliseconds when the the event starts.
+                * @param {int}    endDate     The Unix timestamp in milliseconds when the the event ends.
+                * @param {string} timeZone    A time zone string or offset which is parsable by `wp.date.i18n()`.
+                *
+                * @returns {string}
+                */
+               getFormattedDate: function( startDate, endDate, timeZone ) {
+                       var formattedDate;
+
+                       /*
+                        * The `date_format` option is not used because it's important
+                        * in this context to keep the day of the week in the displayed date,
+                        * so that users can tell at a glance if the event is on a day they
+                        * are available, without having to open the link.
+                        *
+                        * The case of crossing a year boundary is intentionally not handled.
+                        * It's so rare in practice that it's not worth the complexity
+                        * tradeoff. The _ending_ year should be passed to
+                        * `multiple_month_event`, though, just in case.
+                        */
+                       /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */
+                       var singleDayEvent = __( 'l, M j, Y' ),
+                               /* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */
+                               multipleDayEvent = __( '%1$s %2$d–%3$d, %4$d' ),
+                               /* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */
+                               multipleMonthEvent = __( '%1$s %2$d – %3$s %4$d, %5$d' );
+
+                       // Detect single-day events.
+                       if ( ! endDate || format( 'Y-m-d', startDate ) === format( 'Y-m-d', endDate ) ) {
+                               formattedDate = dateI18n( singleDayEvent, startDate, timeZone );
+
+                       // Multiple day events.
+                       } else if ( format( 'Y-m', startDate ) === format( 'Y-m', endDate ) ) {
+                               formattedDate = sprintf(
+                                       multipleDayEvent,
+                                       dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
+                                       dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
+                                       dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
+                                       dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
+                               );
+
+                       // Multi-day events that cross a month boundary.
+                       } else {
+                               formattedDate = sprintf(
+                                       multipleMonthEvent,
+                                       dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ),
+                                       dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ),
+                                       dateI18n( _x( 'F', 'upcoming events month format' ), endDate, timeZone ),
+                                       dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ),
+                                       dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone )
+                               );
+                       }
+
+                       return formattedDate;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpadminincludesclasswpcommunityeventsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-wp-community-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-community-events.php 2020-10-14 18:17:55 UTC (rev 49145)
+++ trunk/src/wp-admin/includes/class-wp-community-events.php   2020-10-14 18:19:43 UTC (rev 49146)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -77,6 +77,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * mitigates possible privacy concerns.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.8.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.6.0 Response no longer contains formatted date field. They're added
+        *              in `wp.communityEvents.populateDynamicEventFields()` now.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $location_search Optional. City name to help determine the location.
</span><span class="cx" style="display: block; padding: 0 10px">         *                                e.g., "Seattle". Default empty string.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -165,7 +167,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->cache_events( $response_body, $expiration );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        $response_body['events'] = $this->trim_events( $response_body['events'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $response_body = $this->format_event_data_time( $response_body );
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        return $response_body;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -344,6 +345,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Gets cached events.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.8.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.6.0 Response no longer contains formatted date field. They're added
+        *              in `wp.communityEvents.populateDynamicEventFields()` now.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|false An array containing `location` and `events` items
</span><span class="cx" style="display: block; padding: 0 10px">         *                     on success, false on failure.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -355,7 +358,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $cached_response['events'] = $this->trim_events( $cached_response['events'] );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->format_event_data_time( $cached_response );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $cached_response;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -372,6 +375,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array The response with dates and times formatted.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected function format_event_data_time( $response_body ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                _deprecated_function(
+                       __METHOD__,
+                       '5.6.0',
+                       'This is no longer used by Core, and only kept for backwards-compatibility.'
+               );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( isset( $response_body['events'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        foreach ( $response_body['events'] as $key => $event ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $timestamp = strtotime( $event['date'] );
</span></span></pre></div>
<a id="trunksrcwpadminincludesdashboardphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/dashboard.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/dashboard.php 2020-10-14 18:17:55 UTC (rev 49145)
+++ trunk/src/wp-admin/includes/dashboard.php   2020-10-14 18:19:43 UTC (rev 49146)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1389,9 +1389,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                </div>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                <div class="event-date-time">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        <span class="event-date">{{ event.formatted_date }}</span>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 <span class="event-date">{{ event.user_formatted_date }}</span>
</ins><span class="cx" style="display: block; padding: 0 10px">                                         <# if ( 'meetup' === event.type ) { #>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                <span class="event-time">{{ event.formatted_time }}</span>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                         <span class="event-time">
+                                                       {{ event.user_formatted_time }} {{ event.timeZoneAbbreviation }}
+                                               </span>
</ins><span class="cx" style="display: block; padding: 0 10px">                                         <# } #>
</span><span class="cx" style="display: block; padding: 0 10px">                                </div>
</span><span class="cx" style="display: block; padding: 0 10px">                        </li>
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2020-10-14 18:17:55 UTC (rev 49145)
+++ trunk/src/wp-includes/script-loader.php     2020-10-14 18:19:43 UTC (rev 49146)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1308,7 +1308,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'wp-color-picker', "/wp-admin/js/color-picker$suffix.js", array( 'iris' ), false, 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->set_translations( 'wp-color-picker' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y' ), false, 1 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y', 'wp-date' ), false, 1 );
+               $scripts->set_translations( 'dashboard' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1755,6 +1756,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'nonce' => wp_create_nonce( 'community_events' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'cache' => $events_client->get_cached_events(),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'time_format' => get_option( 'time_format' ),
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        'l10n'  => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'enter_closest_city'              => __( 'Enter your closest city to find nearby events.' ),
</span></span></pre></div>
<a id="trunktestsqunitindexhtml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/index.html</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/index.html      2020-10-14 18:17:55 UTC (rev 49145)
+++ trunk/tests/qunit/index.html        2020-10-14 18:19:43 UTC (rev 49146)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -85,7 +85,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                </div>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <!-- Tested files -->
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script src="../../build/wp-admin/js/dashboard.js"></script>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <script src="../../build/wp-admin/js/password-strength-meter.js"></script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script src="../../build/wp-admin/js/postbox.js"></script>
+               <script src="../../build/wp-includes/js/dist/vendor/moment.js"></script>
+               <script src="../../build/wp-includes/js/dist/date.js"></script>
+               <script src="../../build/wp-includes/js/dist/i18n.js"></script>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <script src="../../build/wp-includes/js/customize-base.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../build/wp-includes/js/customize-models.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../build/wp-includes/js/shortcode.js"></script>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -142,6 +147,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/password-strength-meter.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/customize-base.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/customize-header.js"></script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script src="wp-admin/js/dashboard.js"></script>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <script src="wp-includes/js/shortcode.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-includes/js/api-request.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-includes/js/wp-api.js"></script>
</span></span></pre></div>
<a id="trunktestsqunitwpadminjsdashboardjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/qunit/wp-admin/js/dashboard.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/dashboard.js                                (rev 0)
+++ trunk/tests/qunit/wp-admin/js/dashboard.js  2020-10-14 18:19:43 UTC (rev 49146)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,219 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global wp, sinon, JSON */
+var communityEventsData, dateI18n, pagenow;
+
+jQuery( document ).ready( function () {
+       var getFormattedDate = wp.communityEvents.getFormattedDate,
+               getTimeZone = wp.communityEvents.getTimeZone,
+               getTimeZoneAbbreviation = wp.communityEvents.getTimeZoneAbbreviation,
+               populateDynamicEventFields = wp.communityEvents.populateDynamicEventFields,
+               startDate = 1600185600 * 1000, // Tue Sep 15 9:00:00 AM PDT 2020
+               HOUR_IN_MS = 60 * 60 * 1000,
+               DAY_IN_MS = HOUR_IN_MS * 24,
+               WEEK_IN_MS = DAY_IN_MS * 7;
+
+       QUnit.module( 'dashboard', function( hooks ) {
+               hooks.beforeEach( function() {
+                       this.oldDateI18n = dateI18n;
+                       this.oldPagenow = pagenow;
+
+                       dateI18n = wp.date.dateI18n;
+                       pagenow = 'dashboard';
+
+                       communityEventsData = {
+                               time_format: 'g:i a',
+                               l10n: {
+                                       date_formats: {
+                                               single_day_event: 'l, M j, Y',
+                                               multiple_day_event: '%1$s %2$d–%3$d, %4$d',
+                                               multiple_month_event: '%1$s %2$d – %3$s %4$d, %5$d'
+                                       }
+                               }
+                       };
+               } );
+
+               hooks.afterEach( function() {
+                       dateI18n = this.oldDateI18n;
+                       pagenow = this.oldPagenow;
+               } );
+
+               QUnit.module( 'communityEvents.populateDynamicEventFields', function() {
+                       QUnit.test( 'dynamic fields should be added', function( assert ) {
+                               var timeFormat = communityEventsData.time_format;
+
+                               var getFormattedDateStub = sinon.stub( wp.communityEvents, 'getFormattedDate' ),
+                                       getTimeZoneStub = sinon.stub( wp.communityEvents, 'getTimeZone' ),
+                                       getTimeZoneAbbreviationStub = sinon.stub( wp.communityEvents, 'getTimeZoneAbbreviation' );
+
+                               getFormattedDateStub.returns( 'Tuesday, Sep 15, 2020' );
+                               getTimeZoneStub.returns( 'America/Chicago' );
+                               getTimeZoneAbbreviationStub.returns( 'CDT' );
+
+                               var rawEvents = [
+                                       {
+                                               start_unix_timestamp: 1600185600,
+                                               end_unix_timestamp: 1600189200
+                                       },
+
+                                       {
+                                               start_unix_timestamp: 1602232400,
+                                               end_unix_timestamp: 1602236000
+                                       }
+                               ];
+
+                               var expected = JSON.parse( JSON.stringify( rawEvents ) );
+                               expected[0].user_formatted_date = 'Tuesday, Sep 15, 2020';
+                               expected[0].user_formatted_time = '11:00 am';
+                               expected[0].timeZoneAbbreviation = 'CDT';
+
+                               expected[1].user_formatted_date = 'Tuesday, Sep 15, 2020'; // This is expected to be the same as item 0, because of the stub.
+                               expected[1].user_formatted_time = '3:33 am';
+                               expected[1].timeZoneAbbreviation = 'CDT';
+
+                               var actual = populateDynamicEventFields( rawEvents, timeFormat );
+
+                               assert.strictEqual(
+                                       JSON.stringify( actual ),
+                                       JSON.stringify( expected )
+                               );
+
+                               getFormattedDateStub.restore();
+                               getTimeZoneStub.restore();
+                               getTimeZoneAbbreviationStub.restore();
+                       } );
+               } );
+
+
+               QUnit.module( 'communityEvents.getFormattedDate', function() {
+                       QUnit.test( 'single month event should use corresponding format', function( assert ) {
+                               var actual = getFormattedDate(
+                                       startDate,
+                                       startDate + HOUR_IN_MS,
+                                       'America/Vancouver',
+                                       communityEventsData.l10n.date_formats
+                               );
+
+                               assert.strictEqual( actual, 'Tuesday, Sep 15, 2020' );
+                       } );
+
+                       QUnit.test( 'multiple day event should use corresponding format', function( assert ) {
+                               var actual = getFormattedDate(
+                                       startDate,
+                                       startDate + ( 2 * DAY_IN_MS ),
+                                       'America/Vancouver',
+                                       communityEventsData.l10n.date_formats
+                               );
+
+                               assert.strictEqual( actual, 'September 15–17, 2020' );
+                       } );
+
+                       QUnit.test( 'multiple month event should use corresponding format', function( assert ) {
+                               var actual = getFormattedDate(
+                                       startDate,
+                                       startDate + ( 3 * WEEK_IN_MS ),
+                                       'America/Vancouver',
+                                       communityEventsData.l10n.date_formats
+                               );
+
+                               assert.strictEqual( actual, 'September 15 – October 6, 2020' );
+                       } );
+
+                       QUnit.test( 'undefined end date should be treated as a single-day event', function( assert ) {
+                               var actual = getFormattedDate(
+                                       startDate,
+                                       undefined,
+                                       'America/Vancouver',
+                                       communityEventsData.l10n.date_formats
+                               );
+
+                               assert.strictEqual( actual, 'Tuesday, Sep 15, 2020' );
+                       } );
+
+                       QUnit.test( 'empty end date should be treated as a single-day event', function( assert ) {
+                               var actual = getFormattedDate(
+                                       startDate,
+                                       '',
+                                       'America/Vancouver',
+                                       communityEventsData.l10n.date_formats
+                               );
+
+                               assert.strictEqual( actual, 'Tuesday, Sep 15, 2020' );
+                       } );
+               } );
+
+
+               QUnit.module( 'communityEvents.getTimeZone', function() {
+                       QUnit.test( 'modern browsers should return a time zone name', function( assert ) {
+                               // Simulate a modern browser.
+                               var stub = sinon.stub( Intl.DateTimeFormat.prototype, 'resolvedOptions' );
+                               stub.returns( { timeZone: 'America/Chicago' } );
+
+                               var actual = getTimeZone( startDate );
+
+                               stub.restore();
+
+                               assert.strictEqual( actual, 'America/Chicago' );
+                       } );
+
+                       QUnit.test( 'older browsers should fallback to a raw UTC offset', function( assert ) {
+                               // Simulate IE11.
+                               var resolvedOptionsStub = sinon.stub( Intl.DateTimeFormat.prototype, 'resolvedOptions' );
+                               var getTimezoneOffsetStub = sinon.stub( Date.prototype, 'getTimezoneOffset' );
+
+                               resolvedOptionsStub.returns( { timeZone: undefined } );
+
+                               getTimezoneOffsetStub.returns( 300 );
+                               var actual = getTimeZone( startDate );
+                               assert.strictEqual( actual, -300, 'negative offset' ); // Intentionally opposite, see `getTimeZone()`.
+
+                               getTimezoneOffsetStub.returns( 0 );
+                               actual = getTimeZone( startDate );
+                               assert.strictEqual( actual, 0, 'no offset' );
+
+                               getTimezoneOffsetStub.returns( -300 );
+                               actual = getTimeZone( startDate );
+                               assert.strictEqual( actual, 300, 'positive offset' ); // Intentionally opposite, see `getTimeZone()`.
+
+                               resolvedOptionsStub.restore();
+                               getTimezoneOffsetStub.restore();
+                       } );
+               } );
+
+
+               QUnit.module( 'communityEvents.getTimeZoneAbbreviation', function() {
+                       QUnit.test( 'modern browsers should return a time zone abbreviation', function( assert ) {
+                               // Modern browsers append a short time zone code to the time string.
+                               var stub = sinon.stub( Date.prototype, 'toLocaleTimeString' );
+                               stub.returns( '4:00:00 PM CDT' );
+
+                               var actual = getTimeZoneAbbreviation( startDate );
+
+                               stub.restore();
+
+                               assert.strictEqual( actual, 'CDT' );
+                       } );
+
+                       QUnit.test( 'older browsers should fallback to a formatted UTC offset', function( assert ) {
+                               var toLocaleTimeStringStub = sinon.stub( Date.prototype, 'toLocaleTimeString' );
+                               var getTimezoneOffsetStub = sinon.stub( Date.prototype, 'getTimezoneOffset' );
+
+                               // IE 11 doesn't add the abbreviation like modern browsers do.
+                               toLocaleTimeStringStub.returns( '4:00:00 PM' );
+
+                               getTimezoneOffsetStub.returns( 300 );
+                               var actual = getTimeZoneAbbreviation( startDate );
+                               assert.strictEqual( actual, 'GMT-5', 'negative offset' ); // Intentionally opposite, see `getTimeZone()`.
+
+                               getTimezoneOffsetStub.returns( 0 );
+                               actual = getTimeZoneAbbreviation( startDate );
+                               assert.strictEqual( actual, 'GMT+0', 'no offset' );
+
+                               getTimezoneOffsetStub.returns( -300 );
+                               actual = getTimeZoneAbbreviation( startDate );
+                               assert.strictEqual( actual, 'GMT+5', 'positive offset' ); // Intentionally opposite, see `getTimeZone()`.
+
+                               toLocaleTimeStringStub.restore();
+                               getTimezoneOffsetStub.restore();
+                       } );
+               } );
+       } );
+} );
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/qunit/wp-admin/js/dashboard.js
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>