<!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>[49275] branches/5.5: 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/49275">49275</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/49275","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>tellyworth</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2020-10-22 04:03:25 +0000 (Thu, 22 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>
Merges <a href="https://core.trac.wordpress.org/changeset/49145">[49145]</a>, <a href="https://core.trac.wordpress.org/changeset/49146">[49146]</a>, <a href="https://core.trac.wordpress.org/changeset/49147">[49147]</a>, <a href="https://core.trac.wordpress.org/changeset/49152">[49152]</a>, and <a href="https://core.trac.wordpress.org/changeset/49201">[49201]</a> to the 5.5 branch.
Props sippis, hlashbrooke, audrasjb, Rarst, iandunn</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#branches55srcjs_enqueueswpdashboardjs">branches/5.5/src/js/_enqueues/wp/dashboard.js</a></li>
<li><a href="#branches55srcwpadminincludesclasswpcommunityeventsphp">branches/5.5/src/wp-admin/includes/class-wp-community-events.php</a></li>
<li><a href="#branches55srcwpadminincludesdashboardphp">branches/5.5/src/wp-admin/includes/dashboard.php</a></li>
<li><a href="#branches55srcwpincludesscriptloaderphp">branches/5.5/src/wp-includes/script-loader.php</a></li>
<li><a href="#branches55testsphpunittestsadminincludesCommunityEventsphp">branches/5.5/tests/phpunit/tests/admin/includesCommunityEvents.php</a></li>
<li><a href="#branches55testsqunitindexhtml">branches/5.5/tests/qunit/index.html</a></li>
</ul>

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

<h3>Property Changed</h3>
<ul>
<li><a href="#branches55">branches/5.5/</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<span class="cx" style="display: block; padding: 0 10px">Index: branches/5.5
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- branches/5.5 2020-10-22 03:36:40 UTC (rev 49274)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ branches/5.5  2020-10-22 04:03:25 UTC (rev 49275)
</ins><a id="branches55"></a>
<div class="propset"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Property changes: branches/5.5</h4>
<pre class="diff"><span>
</span></pre></div>
<a id="svnmergeinfo"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: svn:mergeinfo</h4></div>
<span class="cx" style="display: block; padding: 0 10px"> /branches/3.4:21757
</span><span class="cx" style="display: block; padding: 0 10px"> /branches/4.9:43557,43622
</span><span class="cx" style="display: block; padding: 0 10px"> /branches/5.0:43681-43682,43684-43688,43719-43720,43723,43726-43727,43729-43731,43734-43744,43747,43751-43754,43758,43760-43765,43767-43770,43772,43774-43781,43783,43785,43790-43806,43808-43821,43825,43828,43830-43834,43836-43843,43846-43863,43867-43889,43891-43894,43897-43905,43908-43909,43911-43929,43931-43942,43946-43947,43949-43956,43959-43964,43967-43969,43988,43994,44014,44017,44047,44183,44185,44187-44206,44208-44213,44231-44232,44235,44248,44284,44287-44288
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/trunk:48685-48686,48688-48689,48696,48701,48703,48707,48717,48719,48721,48725-48726,48729-48731,48734,48736,48742,48745,48748-48751,48757-48758,48761,48767,48770,48772,48775,48777-48778,48783,48792,48796-48797,48800-48802,48808-48809,48814,48817,48820,48827,48832,48842,48850,48852-48853,48856,48859,48862,48864,48866,48868,48870,48872,48874,48876,48878,48880-48881,48884,48886,48888,48890,48892,48894-48895,48897,48899,48904,48908-48913,48918,48923-48924,48938,48946,48958,49166,49187,49271
</del><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/trunk:48685-48686,48688-48689,48696,48701,48703,48707,48717,48719,48721,48725-48726,48729-48731,48734,48736,48742,48745,48748-48751,48757-48758,48761,48767,48770,48772,48775,48777-48778,48783,48792,48796-48797,48800-48802,48808-48809,48814,48817,48820,48827,48832,48842,48850,48852-48853,48856,48859,48862,48864,48866,48868,48870,48872,48874,48876,48878,48880-48881,48884,48886,48888,48890,48892,48894-48895,48897,48899,48904,48908-48913,48918,48923-48924,48938,48946,48958,49145-49147,49152,49166,49187,49201,49271
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="branches55srcjs_enqueueswpdashboardjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.5/src/js/_enqueues/wp/dashboard.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.5/src/js/_enqueues/wp/dashboard.js     2020-10-22 03:36:40 UTC (rev 49274)
+++ branches/5.5/src/js/_enqueues/wp/dashboard.js       2020-10-22 04:03:25 UTC (rev 49275)
</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.5.2
+                *
+                * @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.5.2
+                *
+                * @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.5.2
+                *
+                * @param {number} startTimestamp
+                *
+                * @returns {number}
+                */
+               getFlippedTimeZoneOffset: function( startTimestamp ) {
+                       return new Date( startTimestamp ).getTimezoneOffset() * -1;
+               },
+
+               /**
+                * Get a short time zone name, like `PST`.
+                *
+                * @since 5.5.2
+                *
+                * @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.5.2
+                *
+                * @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="branches55srcwpadminincludesclasswpcommunityeventsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.5/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">--- branches/5.5/src/wp-admin/includes/class-wp-community-events.php  2020-10-22 03:36:40 UTC (rev 49274)
+++ branches/5.5/src/wp-admin/includes/class-wp-community-events.php    2020-10-22 04:03:25 UTC (rev 49275)
</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.5.2 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">@@ -158,10 +160,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $response_body['location']['description'] = $this->user_location['description'];
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        /*
+                        * Store the raw response, because events will expire before the cache does.
+                        * The response will need to be processed every page load.
+                        */
</ins><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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $response_body = $this->trim_events( $response_body );
-                       $response_body = $this->format_event_data_time( $response_body );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $response_body['events'] = $this->trim_events( $response_body['events'] );
</ins><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">@@ -340,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.5.2 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">@@ -346,9 +353,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_cached_events() {
</span><span class="cx" style="display: block; padding: 0 10px">                $cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $cached_response = $this->trim_events( $cached_response );
</del><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">+         if ( isset( $cached_response['events'] ) ) {
+                       $cached_response['events'] = $this->trim_events( $cached_response['events'] );
+               }
+
+               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">@@ -360,11 +370,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * of the user who triggered the cache refresh, rather than their own.
</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">+         * @deprecated 5.6.0 No longer used in core.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $response_body The response which contains the events.
</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.5.2',
+                       'This is no longer used by core, and only kept for backward 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 class="lines" style="display: block; padding: 0 10px; color: #888">@@ -435,44 +452,44 @@
</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><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.7 Stick a WordCamp to the final list.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.5.2 Accepts and returns only the events, rather than an entire HTTP response.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array $response_body The response body which contains the events.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array $events The events that will be prepared.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @return array The response body with events trimmed.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function trim_events( $response_body ) {
-               if ( isset( $response_body['events'] ) ) {
-                       $wordcamps = array();
-                       $today     = current_time( 'Y-m-d' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function trim_events( array $events ) {
+               $future_events = array();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        foreach ( $response_body['events'] as $key => $event ) {
-                               /*
-                                * Skip WordCamps, because they might be multi-day events.
-                                * Save a copy so they can be pinned later.
-                                */
-                               if ( 'wordcamp' === $event['type'] ) {
-                                       $wordcamps[] = $event;
-                                       continue;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         foreach ( $events as $event ) {
+                       /*
+                        * The API's `date` and `end_date` fields are in the _event's_ local timezone, but UTC is needed so
+                        * it can be converted to the _user's_ local time.
+                        */
+                       $end_time = (int) $event['end_unix_timestamp'];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                // We don't get accurate time with timezone from API, so we only take the date part (Y-m-d).
-                               $event_date = substr( $event['date'], 0, 10 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( time() < $end_time ) {
+                               array_push( $future_events, $event );
+                       }
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( $today > $event_date ) {
-                                       unset( $response_body['events'][ $key ] );
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $future_wordcamps = array_filter(
+                       $future_events,
+                       function( $wordcamp ) {
+                               return 'wordcamp' === $wordcamp['type'];
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $response_body['events'] = array_slice( $response_body['events'], 0, 3 );
-                       $trimmed_event_types     = wp_list_pluck( $response_body['events'], 'type' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $future_wordcamps    = array_values( $future_wordcamps ); // Remove gaps in indices.
+               $trimmed_events      = array_slice( $future_events, 0, 3 );
+               $trimmed_event_types = wp_list_pluck( $trimmed_events, 'type' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        // Make sure the soonest upcoming WordCamp is pinned in the list.
-                       if ( ! in_array( 'wordcamp', $trimmed_event_types, true ) && $wordcamps ) {
-                               array_pop( $response_body['events'] );
-                               array_push( $response_body['events'], $wordcamps[0] );
-                       }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Make sure the soonest upcoming WordCamp is pinned in the list.
+               if ( $future_wordcamps && ! in_array( 'wordcamp', $trimmed_event_types, true ) ) {
+                       array_pop( $trimmed_events );
+                       array_push( $trimmed_events, $future_wordcamps[0] );
</ins><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 $response_body;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $trimmed_events;
</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="branches55srcwpadminincludesdashboardphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.5/src/wp-admin/includes/dashboard.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.5/src/wp-admin/includes/dashboard.php  2020-10-22 03:36:40 UTC (rev 49274)
+++ branches/5.5/src/wp-admin/includes/dashboard.php    2020-10-22 04:03:25 UTC (rev 49275)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1379,9 +1379,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="branches55srcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.5/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.5/src/wp-includes/script-loader.php    2020-10-22 03:36:40 UTC (rev 49274)
+++ branches/5.5/src/wp-includes/script-loader.php      2020-10-22 04:03:25 UTC (rev 49275)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1277,7 +1277,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">@@ -1722,10 +1723,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'dashboard',
</span><span class="cx" style="display: block; padding: 0 10px">                'communityEventsData',
</span><span class="cx" style="display: block; padding: 0 10px">                array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'nonce' => wp_create_nonce( 'community_events' ),
-                       'cache' => $events_client->get_cached_events(),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'nonce'       => wp_create_nonce( 'community_events' ),
+                       'cache'       => $events_client->get_cached_events(),
+                       'time_format' => get_option( 'time_format' ),
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'l10n'  => array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 'l10n'        => array(
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'enter_closest_city'              => __( 'Enter your closest city to find nearby events.' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'error_occurred_please_try_again' => __( 'An error occurred. Please try again.' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                'attend_event_near_generic'       => __( 'Attend an upcoming event near you.' ),
</span></span></pre></div>
<a id="branches55testsphpunittestsadminincludesCommunityEventsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.5/tests/phpunit/tests/admin/includesCommunityEvents.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.5/tests/phpunit/tests/admin/includesCommunityEvents.php        2020-10-22 03:36:40 UTC (rev 49274)
+++ branches/5.5/tests/phpunit/tests/admin/includesCommunityEvents.php  2020-10-22 04:03:25 UTC (rev 49275)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -153,7 +153,7 @@
</span><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">         * Test: With a valid response, get_events() should return an associative array containing a location array and
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * an events array with individual events that have formatted time and date.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * an events array with individual events that have Unix start/end timestamps.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.8.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -164,15 +164,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotWPError( $response );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEqualSetsWithIndex( $this->get_user_location(), $response['location'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( gmdate( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $response['events'][0]['formatted_date'] );
-               $this->assertEquals( '1:00 pm', $response['events'][0]['formatted_time'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertSame( strtotime( 'next Sunday 1pm' ), $response['events'][0]['start_unix_timestamp'] );
+               $this->assertSame( strtotime( 'next Sunday 2pm' ), $response['events'][0]['end_unix_timestamp'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) );
</span><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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Test: get_cached_events() should return the same data as get_events(), including formatted time
-        * and date values for each event.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Test: `get_cached_events()` should return the same data as get_events(), including Unix start/end
+        * timestamps for each event.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.8.0
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -185,8 +185,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotWPError( $cached_events );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEqualSetsWithIndex( $this->get_user_location(), $cached_events['location'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertEquals( gmdate( 'l, M j, Y', strtotime( 'next Sunday 1pm' ) ), $cached_events['events'][0]['formatted_date'] );
-               $this->assertEquals( '1:00 pm', $cached_events['events'][0]['formatted_time'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertSame( strtotime( 'next Sunday 1pm' ), $cached_events['events'][0]['start_unix_timestamp'] );
+               $this->assertSame( strtotime( 'next Sunday 2pm' ), $cached_events['events'][0]['end_unix_timestamp'] );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response' ) );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -204,50 +204,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'body'     => wp_json_encode(
</span><span class="cx" style="display: block; padding: 0 10px">                                array(
</span><span class="cx" style="display: block; padding: 0 10px">                                        'location' => $this->get_user_location(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        'events'   => array(
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
-                                                       'url'        => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
-                                                       'meetup'     => 'The East Bay WordPress Meetup Group',
-                                                       'meetup_url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Sunday 1pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Oakland, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.808453,
-                                                               'longitude' => -122.26593,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'Part 3- Site Maintenance - Tools to Make It Easy',
-                                                       'url'        => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
-                                                       'meetup'     => 'WordPress Bay Area Foothills Group',
-                                                       'meetup_url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Wednesday 1:30pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Milpitas, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.432813,
-                                                               'longitude' => -121.907095,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'wordcamp',
-                                                       'title'      => 'WordCamp Kansas City',
-                                                       'url'        => 'https://2017.kansascity.wordcamp.org',
-                                                       'meetup'     => null,
-                                                       'meetup_url' => null,
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Saturday' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Kansas City, MO',
-                                                               'country'   => 'US',
-                                                               'latitude'  => 39.0392325,
-                                                               'longitude' => -94.577076,
-                                                       ),
-                                               ),
-                                       ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 'events'   => $this->get_valid_events(),
</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">                        'response' => array(
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -259,103 +216,194 @@
</span><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><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Get a sample of valid events.
+        *
+        * @return array[]
+        */
+       protected function get_valid_events() {
+               return array(
+                       array(
+                               'type'                 => 'meetup',
+                               'title'                => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
+                               'url'                  => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
+                               'meetup'               => 'The East Bay WordPress Meetup Group',
+                               'meetup_url'           => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
+                               'start_unix_timestamp' => strtotime( 'next Sunday 1pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Sunday 2pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Oakland, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.808453,
+                                       'longitude' => -122.26593,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'meetup',
+                               'title'                => 'Part 3- Site Maintenance - Tools to Make It Easy',
+                               'url'                  => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
+                               'meetup'               => 'WordPress Bay Area Foothills Group',
+                               'meetup_url'           => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
+                               'start_unix_timestamp' => strtotime( 'next Wednesday 1:30pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Wednesday 2:30pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Milpitas, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.432813,
+                                       'longitude' => -121.907095,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'wordcamp',
+                               'title'                => 'WordCamp San Francisco',
+                               'url'                  => 'https://sf.wordcamp.org/2020/',
+                               'meetup'               => null,
+                               'meetup_url'           => null,
+                               'start_unix_timestamp' => strtotime( 'next Saturday' ),
+                               'end_unix_timestamp'   => strtotime( 'next Saturday 8pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'San Francisco, CA',
+                                       'country'   => 'US',
+                                       'latitude'  => 37.432813,
+                                       'longitude' => -121.907095,
+                               ),
+                       ),
+               );
+       }
+
+       /**
+        * Test: `trim_events()` should immediately remove expired events.
+        *
+        * @covers WP_Community_Events::trim_events
+        *
+        * @since 5.5.2
+        */
+       public function test_trim_expired_events() {
+               $trim_events = new ReflectionMethod( $this->instance, 'trim_events' );
+               $trim_events->setAccessible( true );
+
+               $events = $this->get_valid_events();
+
+               // This should be removed because it's already ended.
+               $events[0]['start_unix_timestamp'] = strtotime( '1 hour ago' );
+               $events[0]['end_unix_timestamp']   = strtotime( '2 seconds ago' );
+
+               // This should remain because it hasn't ended yet.
+               $events[1]['start_unix_timestamp'] = strtotime( '2 seconds ago' );
+               $events[1]['end_unix_timestamp']   = strtotime( '+1 hour' );
+
+               $actual = $trim_events->invoke( $this->instance, $events );
+
+               $this->assertCount( 2, $actual );
+               $this->assertSame( $actual[0]['title'], 'Part 3- Site Maintenance - Tools to Make It Easy' );
+               $this->assertSame( $actual[1]['title'], 'WordCamp San Francisco' );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Test: get_events() should return the events with the WordCamp pinned in the prepared list.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @covers WP_Community_Events::trim_events
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 4.9.7
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.5.2 Tests `trim_events()` directly instead of indirectly via `get_events()`.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function test_get_events_pin_wordcamp() {
-               add_filter( 'pre_http_request', array( $this, '_http_request_valid_response_unpinned_wordcamp' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_trim_events_pin_wordcamp() {
+               $trim_events = new ReflectionMethod( $this->instance, 'trim_events' );
+               $trim_events->setAccessible( true );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $response_body = $this->instance->get_events();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $actual = $trim_events->invoke( $this->instance, $this->_events_with_unpinned_wordcamp() );
</ins><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">-                 * San Diego was at position 3 in the mock API response, but pinning puts it at position 2,
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+          * San Diego was at index 3 in the mock API response, but pinning puts it at index 2,
</ins><span class="cx" style="display: block; padding: 0 10px">                  * so that it remains in the list. The other events should remain unchanged.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertCount( 3, $response_body['events'] );
-               $this->assertEquals( $response_body['events'][0]['title'], 'Flexbox + CSS Grid: Magic for Responsive Layouts' );
-               $this->assertEquals( $response_body['events'][1]['title'], 'Part 3- Site Maintenance - Tools to Make It Easy' );
-               $this->assertEquals( $response_body['events'][2]['title'], 'WordCamp San Diego' );
-
-               remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response_unpinned_wordcamp' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertCount( 3, $actual );
+               $this->assertSame( $actual[0]['title'], 'Flexbox + CSS Grid: Magic for Responsive Layouts' );
+               $this->assertSame( $actual[1]['title'], 'Part 3- Site Maintenance - Tools to Make It Easy' );
+               $this->assertSame( $actual[2]['title'], 'WordCamp San Diego' );
</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><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Simulates a valid HTTP response where a WordCamp needs to be pinned higher than it's default position.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Simulates a scenario where a WordCamp needs to be pinned higher than it's default position.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.7
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.5.2 Accepts and returns only the events, rather than an entire HTTP response.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @return array A mock HTTP response.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @return array A list of mock events.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function _http_request_valid_response_unpinned_wordcamp() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function _events_with_unpinned_wordcamp() {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'headers'  => '',
-                       'response' => array( 'code' => 200 ),
-                       'cookies'  => '',
-                       'filename' => '',
-                       'body'     => wp_json_encode(
-                               array(
-                                       'location' => $this->get_user_location(),
-                                       'events'   => array(
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
-                                                       'url'        => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
-                                                       'meetup'     => 'The East Bay WordPress Meetup Group',
-                                                       'meetup_url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Monday 1pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Oakland, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.808453,
-                                                               'longitude' => -122.26593,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'Part 3- Site Maintenance - Tools to Make It Easy',
-                                                       'url'        => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
-                                                       'meetup'     => 'WordPress Bay Area Foothills Group',
-                                                       'meetup_url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Tuesday 1:30pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Milpitas, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.432813,
-                                                               'longitude' => -121.907095,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'WordPress Q&A',
-                                                       'url'        => 'https://www.meetup.com/sanjosewp/events/245419844/',
-                                                       'meetup'     => 'The San Jose WordPress Meetup',
-                                                       'meetup_url' => 'https://www.meetup.com/sanjosewp/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Wednesday 5:30pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Milpitas, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.244194,
-                                                               'longitude' => -121.889313,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'wordcamp',
-                                                       'title'      => 'WordCamp San Diego',
-                                                       'url'        => 'https://2018.sandiego.wordcamp.org',
-                                                       'meetup'     => null,
-                                                       'meetup_url' => null,
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Thursday 9am' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'San Diego, CA',
-                                                               'country'   => 'US',
-                                                               'latitude'  => 32.7220419,
-                                                               'longitude' => -117.1534513,
-                                                       ),
-                                               ),
-                                       ),
-                               )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 array(
+                               'type'                 => 'meetup',
+                               'title'                => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
+                               'url'                  => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
+                               'meetup'               => 'The East Bay WordPress Meetup Group',
+                               'meetup_url'           => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
+                               'start_unix_timestamp' => strtotime( 'next Monday 1pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Monday 2pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Oakland, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.808453,
+                                       'longitude' => -122.26593,
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       array(
+                               'type'                 => 'meetup',
+                               'title'                => 'Part 3- Site Maintenance - Tools to Make It Easy',
+                               'url'                  => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
+                               'meetup'               => 'WordPress Bay Area Foothills Group',
+                               'meetup_url'           => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
+                               'start_unix_timestamp' => strtotime( 'next Tuesday 1:30pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Tuesday 2:30pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Milpitas, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.432813,
+                                       'longitude' => -121.907095,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'meetup',
+                               'title'                => 'WordPress Q&A',
+                               'url'                  => 'https://www.meetup.com/sanjosewp/events/245419844/',
+                               'meetup'               => 'The San Jose WordPress Meetup',
+                               'meetup_url'           => 'https://www.meetup.com/sanjosewp/',
+                               'start_unix_timestamp' => strtotime( 'next Wednesday 5:30pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Wednesday 6:30pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Milpitas, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.244194,
+                                       'longitude' => -121.889313,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'wordcamp',
+                               'title'                => 'WordCamp San Diego',
+                               'url'                  => 'https://2018.sandiego.wordcamp.org',
+                               'meetup'               => null,
+                               'meetup_url'           => null,
+                               'start_unix_timestamp' => strtotime( 'next Thursday 9am' ),
+                               'end_unix_timestamp'   => strtotime( 'next Thursday 10am' ),
+
+                               'location'             => array(
+                                       'location'  => 'San Diego, CA',
+                                       'country'   => 'US',
+                                       'latitude'  => 32.7220419,
+                                       'longitude' => -117.1534513,
+                               ),
+                       ),
</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">@@ -363,23 +411,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Test: get_events() shouldn't stick an extra WordCamp when there's already one that naturally
</span><span class="cx" style="display: block; padding: 0 10px">         * falls into the list.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @covers WP_Community_Events::trim_events
+        *
</ins><span class="cx" style="display: block; padding: 0 10px">          * @since 4.9.7
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.5.2 Tests `trim_events()` directly instead of indirectly via `get_events()`.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function test_get_events_dont_pin_multiple_wordcamps() {
-               add_filter( 'pre_http_request', array( $this, '_http_request_valid_response_multiple_wordcamps' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function test_trim_events_dont_pin_multiple_wordcamps() {
+               $trim_events = new ReflectionMethod( $this->instance, 'trim_events' );
+               $trim_events->setAccessible( true );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $response_body = $this->instance->get_events();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $actual = $trim_events->invoke( $this->instance, $this->_events_with_multiple_wordcamps() );
</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">                 * The first meetup should be removed because it's expired, while the next 3 events are selected.
</span><span class="cx" style="display: block; padding: 0 10px">                 * WordCamp LA should not be stuck to the list, because San Diego already appears naturally.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $this->assertCount( 3, $response_body['events'] );
-               $this->assertEquals( $response_body['events'][0]['title'], 'WordCamp San Diego' );
-               $this->assertEquals( $response_body['events'][1]['title'], 'Part 3- Site Maintenance - Tools to Make It Easy' );
-               $this->assertEquals( $response_body['events'][2]['title'], 'WordPress Q&A' );
-
-               remove_filter( 'pre_http_request', array( $this, '_http_request_valid_response_multiple_wordcamps' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->assertCount( 3, $actual );
+               $this->assertSame( $actual[0]['title'], 'WordCamp San Diego' );
+               $this->assertSame( $actual[1]['title'], 'Part 3- Site Maintenance - Tools to Make It Easy' );
+               $this->assertSame( $actual[2]['title'], 'WordPress Q&A' );
</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">@@ -387,92 +437,96 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * no need to pin extra camp b/c one already exists in response
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.9.7
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.5.2 Tests `trim_events()` directly instead of indirectly via `get_events()`.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array A mock HTTP response.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        public function _http_request_valid_response_multiple_wordcamps() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function _events_with_multiple_wordcamps() {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'headers'  => '',
-                       'response' => array( 'code' => 200 ),
-                       'cookies'  => '',
-                       'filename' => '',
-                       'body'     => wp_json_encode(
-                               array(
-                                       'location' => $this->get_user_location(),
-                                       'events'   => array(
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
-                                                       'url'        => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
-                                                       'meetup'     => 'The East Bay WordPress Meetup Group',
-                                                       'meetup_url' => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( '2 days ago' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Oakland, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.808453,
-                                                               'longitude' => -122.26593,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'wordcamp',
-                                                       'title'      => 'WordCamp San Diego',
-                                                       'url'        => 'https://2018.sandiego.wordcamp.org',
-                                                       'meetup'     => null,
-                                                       'meetup_url' => null,
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Tuesday 9am' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'San Diego, CA',
-                                                               'country'   => 'US',
-                                                               'latitude'  => 32.7220419,
-                                                               'longitude' => -117.1534513,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'Part 3- Site Maintenance - Tools to Make It Easy',
-                                                       'url'        => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
-                                                       'meetup'     => 'WordPress Bay Area Foothills Group',
-                                                       'meetup_url' => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Wednesday 1:30pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Milpitas, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.432813,
-                                                               'longitude' => -121.907095,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'meetup',
-                                                       'title'      => 'WordPress Q&A',
-                                                       'url'        => 'https://www.meetup.com/sanjosewp/events/245419844/',
-                                                       'meetup'     => 'The San Jose WordPress Meetup',
-                                                       'meetup_url' => 'https://www.meetup.com/sanjosewp/',
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Thursday 5:30pm' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Milpitas, CA, USA',
-                                                               'country'   => 'us',
-                                                               'latitude'  => 37.244194,
-                                                               'longitude' => -121.889313,
-                                                       ),
-                                               ),
-                                               array(
-                                                       'type'       => 'wordcamp',
-                                                       'title'      => 'WordCamp Los Angeles',
-                                                       'url'        => 'https://2018.la.wordcamp.org',
-                                                       'meetup'     => null,
-                                                       'meetup_url' => null,
-                                                       'date'       => gmdate( 'Y-m-d H:i:s', strtotime( 'next Friday 9am' ) ),
-                                                       'location'   => array(
-                                                               'location'  => 'Los Angeles, CA',
-                                                               'country'   => 'US',
-                                                               'latitude'  => 34.050888,
-                                                               'longitude' => -118.285426,
-                                                       ),
-                                               ),
-                                       ),
-                               )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 array(
+                               'type'                 => 'meetup',
+                               'title'                => 'Flexbox + CSS Grid: Magic for Responsive Layouts',
+                               'url'                  => 'https://www.meetup.com/Eastbay-WordPress-Meetup/events/236031233/',
+                               'meetup'               => 'The East Bay WordPress Meetup Group',
+                               'meetup_url'           => 'https://www.meetup.com/Eastbay-WordPress-Meetup/',
+                               'start_unix_timestamp' => strtotime( '2 days ago' ) - HOUR_IN_SECONDS,
+                               'end_unix_timestamp'   => strtotime( '2 days ago' ),
+
+                               'location'             => array(
+                                       'location'  => 'Oakland, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.808453,
+                                       'longitude' => -122.26593,
+                               ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       array(
+                               'type'                 => 'wordcamp',
+                               'title'                => 'WordCamp San Diego',
+                               'url'                  => 'https://2018.sandiego.wordcamp.org',
+                               'meetup'               => null,
+                               'meetup_url'           => null,
+                               'start_unix_timestamp' => strtotime( 'next Tuesday 9am' ),
+                               'end_unix_timestamp'   => strtotime( 'next Tuesday 10am' ),
+
+                               'location'             => array(
+                                       'location'  => 'San Diego, CA',
+                                       'country'   => 'US',
+                                       'latitude'  => 32.7220419,
+                                       'longitude' => -117.1534513,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'meetup',
+                               'title'                => 'Part 3- Site Maintenance - Tools to Make It Easy',
+                               'url'                  => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/events/237706839/',
+                               'meetup'               => 'WordPress Bay Area Foothills Group',
+                               'meetup_url'           => 'https://www.meetup.com/Wordpress-Bay-Area-CA-Foothills/',
+                               'start_unix_timestamp' => strtotime( 'next Wednesday 1:30pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Wednesday 2:30pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Milpitas, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.432813,
+                                       'longitude' => -121.907095,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'meetup',
+                               'title'                => 'WordPress Q&A',
+                               'url'                  => 'https://www.meetup.com/sanjosewp/events/245419844/',
+                               'meetup'               => 'The San Jose WordPress Meetup',
+                               'meetup_url'           => 'https://www.meetup.com/sanjosewp/',
+                               'start_unix_timestamp' => strtotime( 'next Thursday 5:30pm' ),
+                               'end_unix_timestamp'   => strtotime( 'next Thursday 6:30pm' ),
+
+                               'location'             => array(
+                                       'location'  => 'Milpitas, CA, USA',
+                                       'country'   => 'us',
+                                       'latitude'  => 37.244194,
+                                       'longitude' => -121.889313,
+                               ),
+                       ),
+
+                       array(
+                               'type'                 => 'wordcamp',
+                               'title'                => 'WordCamp Los Angeles',
+                               'url'                  => 'https://2018.la.wordcamp.org',
+                               'meetup'               => null,
+                               'meetup_url'           => null,
+                               'start_unix_timestamp' => strtotime( 'next Friday 9am' ),
+                               'end_unix_timestamp'   => strtotime( 'next Friday 10am' ),
+
+                               'location'             => array(
+                                       'location'  => 'Los Angeles, CA',
+                                       'country'   => 'US',
+                                       'latitude'  => 34.050888,
+                                       'longitude' => -118.285426,
+                               ),
+                       ),
</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="branches55testsqunitindexhtml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.5/tests/qunit/index.html</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.5/tests/qunit/index.html       2020-10-22 03:36:40 UTC (rev 49274)
+++ branches/5.5/tests/qunit/index.html 2020-10-22 04:03:25 UTC (rev 49275)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -83,7 +83,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">@@ -141,6 +146,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="branches55testsqunitwpadminjsdashboardjsfromrev49146trunktestsqunitwpadminjsdashboardjs"></a>
<div class="copfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Copied: branches/5.5/tests/qunit/wp-admin/js/dashboard.js (from rev 49146, trunk/tests/qunit/wp-admin/js/dashboard.js)</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.5/tests/qunit/wp-admin/js/dashboard.js                         (rev 0)
+++ branches/5.5/tests/qunit/wp-admin/js/dashboard.js   2020-10-22 04:03:25 UTC (rev 49275)
</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></pre>
</div>
</div>

</body>
</html>