Community Events: Display dates and times in the user's time zone.

Fixes #51130
Merges [49145], [49146], [49147], [49152], and [49201] to the 5.5 branch.
Props sippis, hlashbrooke, audrasjb, Rarst, iandunn

Built from https://develop.svn.wordpress.org/branches/5.5@49275


git-svn-id: http://core.svn.wordpress.org/branches/5.5@49037 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
tellyworth 2020-10-22 04:05:05 +00:00
parent 59ca490c66
commit 542c3d4bb1
6 changed files with 263 additions and 42 deletions

View File

@ -77,6 +77,8 @@ class WP_Community_Events {
* mitigates possible privacy concerns.
*
* @since 4.8.0
* @since 5.5.2 Response no longer contains formatted date field. They're added
* in `wp.communityEvents.populateDynamicEventFields()` now.
*
* @param string $location_search Optional. City name to help determine the location.
* e.g., "Seattle". Default empty string.
@ -158,10 +160,13 @@ class WP_Community_Events {
$response_body['location']['description'] = $this->user_location['description'];
}
/*
* Store the raw response, because events will expire before the cache does.
* The response will need to be processed every page load.
*/
$this->cache_events( $response_body, $expiration );
$response_body = $this->trim_events( $response_body );
$response_body = $this->format_event_data_time( $response_body );
$response_body['events'] = $this->trim_events( $response_body['events'] );
return $response_body;
}
@ -340,15 +345,20 @@ class WP_Community_Events {
* Gets cached events.
*
* @since 4.8.0
* @since 5.5.2 Response no longer contains formatted date field. They're added
* in `wp.communityEvents.populateDynamicEventFields()` now.
*
* @return array|false An array containing `location` and `events` items
* on success, false on failure.
*/
public function get_cached_events() {
$cached_response = get_site_transient( $this->get_events_transient_key( $this->user_location ) );
$cached_response = $this->trim_events( $cached_response );
return $this->format_event_data_time( $cached_response );
if ( isset( $cached_response['events'] ) ) {
$cached_response['events'] = $this->trim_events( $cached_response['events'] );
}
return $cached_response;
}
/**
@ -360,11 +370,18 @@ class WP_Community_Events {
* of the user who triggered the cache refresh, rather than their own.
*
* @since 4.8.0
* @deprecated 5.6.0 No longer used in core.
*
* @param array $response_body The response which contains the events.
* @return array The response with dates and times formatted.
*/
protected function format_event_data_time( $response_body ) {
_deprecated_function(
__METHOD__,
'5.5.2',
'This is no longer used by core, and only kept for backward compatibility.'
);
if ( isset( $response_body['events'] ) ) {
foreach ( $response_body['events'] as $key => $event ) {
$timestamp = strtotime( $event['date'] );
@ -435,44 +452,44 @@ class WP_Community_Events {
*
* @since 4.8.0
* @since 4.9.7 Stick a WordCamp to the final list.
* @since 5.5.2 Accepts and returns only the events, rather than an entire HTTP response.
*
* @param array $response_body The response body which contains the events.
* @param array $events The events that will be prepared.
* @return array The response body with events trimmed.
*/
protected function trim_events( $response_body ) {
if ( isset( $response_body['events'] ) ) {
$wordcamps = array();
$today = current_time( 'Y-m-d' );
protected function trim_events( array $events ) {
$future_events = array();
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;
}
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'];
// 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 );
if ( $today > $event_date ) {
unset( $response_body['events'][ $key ] );
}
}
$response_body['events'] = array_slice( $response_body['events'], 0, 3 );
$trimmed_event_types = wp_list_pluck( $response_body['events'], 'type' );
// 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] );
if ( time() < $end_time ) {
array_push( $future_events, $event );
}
}
return $response_body;
$future_wordcamps = array_filter(
$future_events,
function( $wordcamp ) {
return 'wordcamp' === $wordcamp['type'];
}
);
$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' );
// 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] );
}
return $trimmed_events;
}
/**

View File

@ -1379,9 +1379,11 @@ function wp_print_community_events_templates() {
</div>
<div class="event-date-time">
<span class="event-date">{{ event.formatted_date }}</span>
<span class="event-date">{{ event.user_formatted_date }}</span>
<# if ( 'meetup' === event.type ) { #>
<span class="event-time">{{ event.formatted_time }}</span>
<span class="event-time">
{{ event.user_formatted_time }} {{ event.timeZoneAbbreviation }}
</span>
<# } #>
</div>
</li>

View File

@ -266,6 +266,11 @@ jQuery( function( $ ) {
'use strict';
var communityEventsData = window.communityEventsData || {},
dateI18n = wp.date.dateI18n,
format = wp.date.format,
sprintf = wp.i18n.sprintf,
__ = wp.i18n.__,
_x = wp.i18n._x,
app;
/**
@ -441,6 +446,7 @@ jQuery( function( $ ) {
.fail( function() {
app.renderEventsTemplate({
'location' : false,
'events' : [],
'error' : true
}, initiatedBy );
});
@ -465,6 +471,11 @@ jQuery( function( $ ) {
$locationMessage = $( '#community-events-location-message' ),
$results = $( '.community-events-results' );
templateParams.events = app.populateDynamicEventFields(
templateParams.events,
communityEventsData.time_format
);
/*
* Hide all toggleable elements by default, to keep the logic simple.
* Otherwise, each block below would have to turn hide everything that
@ -576,6 +587,195 @@ jQuery( function( $ ) {
} else {
app.toggleLocationForm( 'show' );
}
},
/**
* 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;
}
};

File diff suppressed because one or more lines are too long

View File

@ -1277,7 +1277,8 @@ function wp_default_scripts( $scripts ) {
$scripts->add( 'wp-color-picker', "/wp-admin/js/color-picker$suffix.js", array( 'iris' ), false, 1 );
$scripts->set_translations( 'wp-color-picker' );
$scripts->add( 'dashboard', "/wp-admin/js/dashboard$suffix.js", array( 'jquery', 'admin-comments', 'postbox', 'wp-util', 'wp-a11y' ), false, 1 );
$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' );
$scripts->add( 'list-revisions', "/wp-includes/js/wp-list-revisions$suffix.js" );
@ -1722,10 +1723,11 @@ function wp_localize_community_events() {
'dashboard',
'communityEventsData',
array(
'nonce' => wp_create_nonce( 'community_events' ),
'cache' => $events_client->get_cached_events(),
'nonce' => wp_create_nonce( 'community_events' ),
'cache' => $events_client->get_cached_events(),
'time_format' => get_option( 'time_format' ),
'l10n' => array(
'l10n' => array(
'enter_closest_city' => __( 'Enter your closest city to find nearby events.' ),
'error_occurred_please_try_again' => __( 'An error occurred. Please try again.' ),
'attend_event_near_generic' => __( 'Attend an upcoming event near you.' ),

View File

@ -13,7 +13,7 @@
*
* @global string $wp_version
*/
$wp_version = '5.5.2-alpha-49273';
$wp_version = '5.5.2-alpha-49275';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.