mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 03:51:07 +08:00
REFACTOR: local dates to improve reliability with DST and recurrence (#9379)
This commit improves testing and separates local-date generation from dates with zone manipulations.
This commit is contained in:
@ -1,357 +0,0 @@
|
||||
// discourse-skip-module
|
||||
(function($) {
|
||||
const DATE_TEMPLATE = `
|
||||
<span>
|
||||
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="#globe-americas"></use>
|
||||
</svg>
|
||||
<span class="relative-time"></span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
const PREVIEW_TEMPLATE = `
|
||||
<div class='preview'>
|
||||
<span class='timezone'></span>
|
||||
<span class='date-time'></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function processElement($element, options = {}) {
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
const utc = moment().utc();
|
||||
const dateTime = options.time
|
||||
? `${options.date} ${options.time}`
|
||||
: options.date;
|
||||
|
||||
let displayedTimezone;
|
||||
if (options.time) {
|
||||
displayedTimezone = options.displayedTimezone || moment.tz.guess();
|
||||
} else {
|
||||
displayedTimezone =
|
||||
options.displayedTimezone || options.timezone || moment.tz.guess();
|
||||
}
|
||||
|
||||
// if timezone given we convert date and time from given zone to Etc/UTC
|
||||
let utcDateTime;
|
||||
if (options.timezone) {
|
||||
utcDateTime = _applyZoneToDateTime(dateTime, options.timezone);
|
||||
} else {
|
||||
utcDateTime = moment.utc(dateTime);
|
||||
}
|
||||
|
||||
if (utcDateTime < utc) {
|
||||
// if event is in the past we want to bump it no next occurrence when
|
||||
// recurring is set
|
||||
if (options.recurring) {
|
||||
utcDateTime = _applyRecurrence(utcDateTime, options);
|
||||
} else {
|
||||
$element.addClass("past");
|
||||
}
|
||||
}
|
||||
|
||||
// once we have the correct UTC date we want
|
||||
// we adjust it to watching user timezone
|
||||
const adjustedDateTime = utcDateTime.tz(displayedTimezone);
|
||||
|
||||
const previews = _generatePreviews(
|
||||
adjustedDateTime.clone(),
|
||||
displayedTimezone,
|
||||
options
|
||||
);
|
||||
const textPreview = _generateTextPreview(previews);
|
||||
const htmlPreview = _generateHtmlPreview(previews);
|
||||
|
||||
const formatedDateTime = _applyFormatting(
|
||||
adjustedDateTime,
|
||||
displayedTimezone,
|
||||
options
|
||||
);
|
||||
|
||||
$element
|
||||
.html(DATE_TEMPLATE)
|
||||
.attr("aria-label", textPreview)
|
||||
.attr(
|
||||
"data-html-tooltip",
|
||||
`<div class="locale-dates-previews">${htmlPreview}</div>`
|
||||
)
|
||||
.addClass("cooked-date")
|
||||
.find(".relative-time")
|
||||
.text(formatedDateTime);
|
||||
|
||||
this.timeout = setTimeout(
|
||||
() => processElement($element, options),
|
||||
60 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
function _formatTimezone(timezone) {
|
||||
return timezone
|
||||
.replace("_", " ")
|
||||
.replace("Etc/", "")
|
||||
.split("/");
|
||||
}
|
||||
|
||||
function _zoneWithoutPrefix(timezone) {
|
||||
const parts = _formatTimezone(timezone);
|
||||
return parts[1] || parts[0];
|
||||
}
|
||||
|
||||
function _applyZoneToDateTime(dateTime, timezone) {
|
||||
return moment.tz(dateTime, timezone).utc();
|
||||
}
|
||||
|
||||
function _translateCalendarKey(time, key) {
|
||||
const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, {
|
||||
time: "LT"
|
||||
});
|
||||
|
||||
if (time) {
|
||||
return translated
|
||||
.split("LT")
|
||||
.map(w => `[${w}]`)
|
||||
.join("LT");
|
||||
} else {
|
||||
return `[${translated.replace(" LT", "")}]`;
|
||||
}
|
||||
}
|
||||
|
||||
function _calendarFormats(time) {
|
||||
return {
|
||||
sameDay: _translateCalendarKey(time, "today"),
|
||||
nextDay: _translateCalendarKey(time, "tomorrow"),
|
||||
lastDay: _translateCalendarKey(time, "yesterday"),
|
||||
sameElse: "L"
|
||||
};
|
||||
}
|
||||
|
||||
function _isEqualZones(timezoneA, timezoneB) {
|
||||
if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset()
|
||||
);
|
||||
}
|
||||
|
||||
function _applyFormatting(dateTime, displayedTimezone, options) {
|
||||
if (options.countdown) {
|
||||
const diffTime = dateTime.diff(moment());
|
||||
if (diffTime < 0) {
|
||||
return I18n.t("discourse_local_dates.relative_dates.countdown.passed");
|
||||
} else {
|
||||
return moment.duration(diffTime).humanize();
|
||||
}
|
||||
}
|
||||
|
||||
const sameTimezone = _isEqualZones(displayedTimezone, moment.tz.guess());
|
||||
const inCalendarRange = dateTime.isBetween(
|
||||
moment().subtract(2, "days"),
|
||||
moment()
|
||||
.add(1, "days")
|
||||
.endOf("day")
|
||||
);
|
||||
|
||||
if (options.calendar && inCalendarRange) {
|
||||
if (sameTimezone) {
|
||||
if (options.time) {
|
||||
dateTime = dateTime.calendar(null, _calendarFormats(options.time));
|
||||
} else {
|
||||
dateTime = dateTime.calendar(null, _calendarFormats(null));
|
||||
}
|
||||
} else {
|
||||
dateTime = dateTime.format(options.format);
|
||||
dateTime = dateTime.replace("TZ", "");
|
||||
dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`;
|
||||
}
|
||||
} else {
|
||||
if (options.time) {
|
||||
dateTime = dateTime.format(options.format);
|
||||
|
||||
if (options.displayedTimezone && !sameTimezone) {
|
||||
dateTime = dateTime.replace("TZ", "");
|
||||
dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`;
|
||||
} else {
|
||||
dateTime = dateTime.replace(
|
||||
"TZ",
|
||||
_formatTimezone(displayedTimezone).join(": ")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dateTime = dateTime.format(options.format);
|
||||
|
||||
if (!sameTimezone) {
|
||||
dateTime = dateTime.replace("TZ", "");
|
||||
dateTime = `${dateTime} (${_zoneWithoutPrefix(displayedTimezone)})`;
|
||||
} else {
|
||||
dateTime = dateTime.replace(
|
||||
"TZ",
|
||||
_zoneWithoutPrefix(displayedTimezone)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
function _applyRecurrence(dateTime, { recurring, timezone }) {
|
||||
const parts = recurring.split(".");
|
||||
const count = parseInt(parts[0], 10);
|
||||
const type = parts[1];
|
||||
const diff = moment().diff(dateTime, type);
|
||||
const add = Math.ceil(diff + count);
|
||||
|
||||
// we create new moment object from format
|
||||
// to ensure it's created in user context
|
||||
const wasDST = moment(dateTime.format()).isDST();
|
||||
let dateTimeWithRecurrence = moment(dateTime).add(add, type);
|
||||
const isDST = moment(dateTimeWithRecurrence.format()).isDST();
|
||||
|
||||
// these dates are more or less DST "certain"
|
||||
const noDSTOffset = moment
|
||||
.tz({ month: 0, day: 1 }, timezone || "Etc/UTC")
|
||||
.utcOffset();
|
||||
const withDSTOffset = moment
|
||||
.tz({ month: 5, day: 1 }, timezone || "Etc/UTC")
|
||||
.utcOffset();
|
||||
|
||||
// we remove the DST offset present when the date was created,
|
||||
// and add current DST offset
|
||||
if (!wasDST && isDST) {
|
||||
dateTimeWithRecurrence.add(-withDSTOffset + noDSTOffset, "minutes");
|
||||
}
|
||||
|
||||
// we add the DST offset present when the date was created,
|
||||
// and remove current DST offset
|
||||
if (wasDST && !isDST) {
|
||||
dateTimeWithRecurrence.add(withDSTOffset - noDSTOffset, "minutes");
|
||||
}
|
||||
|
||||
return dateTimeWithRecurrence;
|
||||
}
|
||||
|
||||
function _createDateTimeRange(dateTime, timezone) {
|
||||
const dt = moment(dateTime).tz(timezone);
|
||||
|
||||
return [dt.format("LLL"), "→", dt.add(24, "hours").format("LLL")].join(" ");
|
||||
}
|
||||
|
||||
function _generatePreviews(dateTime, displayedTimezone, options) {
|
||||
const previewedTimezones = [];
|
||||
const watchingUserTimezone = moment.tz.guess();
|
||||
const timezones = options.timezones.filter(
|
||||
timezone =>
|
||||
!_isEqualZones(timezone, watchingUserTimezone) &&
|
||||
!_isEqualZones(timezone, options.timezone)
|
||||
);
|
||||
|
||||
previewedTimezones.push({
|
||||
timezone: watchingUserTimezone,
|
||||
current: true,
|
||||
dateTime: options.time
|
||||
? moment(dateTime)
|
||||
.tz(watchingUserTimezone)
|
||||
.format("LLL")
|
||||
: _createDateTimeRange(dateTime, watchingUserTimezone)
|
||||
});
|
||||
|
||||
if (
|
||||
options.timezone &&
|
||||
displayedTimezone === watchingUserTimezone &&
|
||||
options.timezone !== displayedTimezone &&
|
||||
!_isEqualZones(displayedTimezone, options.timezone)
|
||||
) {
|
||||
timezones.unshift(options.timezone);
|
||||
}
|
||||
|
||||
Array.from(new Set(timezones.filter(Boolean))).forEach(timezone => {
|
||||
if (_isEqualZones(timezone, displayedTimezone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isEqualZones(timezone, watchingUserTimezone)) {
|
||||
timezone = watchingUserTimezone;
|
||||
}
|
||||
|
||||
previewedTimezones.push({
|
||||
timezone,
|
||||
dateTime: options.time
|
||||
? moment(dateTime)
|
||||
.tz(timezone)
|
||||
.format("LLL")
|
||||
: _createDateTimeRange(dateTime, timezone)
|
||||
});
|
||||
});
|
||||
|
||||
if (!previewedTimezones.length) {
|
||||
previewedTimezones.push({
|
||||
timezone: "Etc/UTC",
|
||||
dateTime: options.time
|
||||
? moment(dateTime)
|
||||
.tz("Etc/UTC")
|
||||
.format("LLL")
|
||||
: _createDateTimeRange(dateTime, "Etc/UTC")
|
||||
});
|
||||
}
|
||||
|
||||
return _.uniq(previewedTimezones, "timezone");
|
||||
}
|
||||
|
||||
function _generateTextPreview(previews) {
|
||||
return previews
|
||||
.map(preview => {
|
||||
const formatedZone = _zoneWithoutPrefix(preview.timezone);
|
||||
|
||||
if (preview.dateTime.match(/TZ/)) {
|
||||
return preview.dateTime.replace(/TZ/, formatedZone);
|
||||
} else {
|
||||
return `${formatedZone} ${preview.dateTime}`;
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function _generateHtmlPreview(previews) {
|
||||
return previews
|
||||
.map(preview => {
|
||||
const $template = $(PREVIEW_TEMPLATE);
|
||||
|
||||
if (preview.current) $template.addClass("current");
|
||||
|
||||
$template.find(".timezone").text(_zoneWithoutPrefix(preview.timezone));
|
||||
$template.find(".date-time").text(preview.dateTime);
|
||||
return $template[0].outerHTML;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
$.fn.applyLocalDates = function() {
|
||||
return this.each(function() {
|
||||
const $element = $(this);
|
||||
|
||||
const options = {};
|
||||
options.time = $element.attr("data-time");
|
||||
options.date = $element.attr("data-date");
|
||||
options.recurring = $element.attr("data-recurring");
|
||||
options.timezones = (
|
||||
$element.attr("data-timezones") ||
|
||||
Discourse.SiteSettings.discourse_local_dates_default_timezones ||
|
||||
"Etc/UTC"
|
||||
).split("|");
|
||||
options.timezone = $element.attr("data-timezone");
|
||||
options.calendar = ($element.attr("data-calendar") || "on") === "on";
|
||||
options.displayedTimezone = $element.attr("data-displayed-timezone");
|
||||
options.format =
|
||||
$element.attr("data-format") || (options.time ? "LLL" : "LL");
|
||||
options.countdown = $element.attr("data-countdown");
|
||||
|
||||
processElement($element, options);
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
@ -1,11 +1,19 @@
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import LocalDateBuilder from "../lib/local-date-builder";
|
||||
|
||||
const DATE_TEMPLATE = `
|
||||
<span>
|
||||
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<use xlink:href="#globe-americas"></use>
|
||||
</svg>
|
||||
<span class="relative-time"></span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
function initializeDiscourseLocalDates(api) {
|
||||
api.decorateCooked(
|
||||
$elem => {
|
||||
$(".discourse-local-date", $elem).applyLocalDates();
|
||||
},
|
||||
$elem => $(".discourse-local-date", $elem).applyLocalDates(),
|
||||
{ id: "discourse-local-date" }
|
||||
);
|
||||
|
||||
@ -37,6 +45,69 @@ export default {
|
||||
initialize(container) {
|
||||
const siteSettings = container.lookup("site-settings:main");
|
||||
if (siteSettings.discourse_local_dates_enabled) {
|
||||
$.fn.applyLocalDates = function() {
|
||||
return this.each(function() {
|
||||
const opts = {};
|
||||
const dataset = this.dataset;
|
||||
opts.time = dataset.time;
|
||||
opts.date = dataset.date;
|
||||
opts.recurring = dataset.recurring;
|
||||
opts.timezones = (
|
||||
dataset.timezones ||
|
||||
siteSettings.discourse_local_dates_default_timezones ||
|
||||
"Etc/UTC"
|
||||
)
|
||||
.split("|")
|
||||
.filter(Boolean);
|
||||
opts.timezone = dataset.timezone;
|
||||
opts.calendar = (dataset.calendar || "on") === "on";
|
||||
opts.displayedTimezone = dataset.displayedTimezone;
|
||||
opts.format = dataset.format || (opts.time ? "LLL" : "LL");
|
||||
opts.countdown = dataset.countdown;
|
||||
|
||||
const localDateBuilder = new LocalDateBuilder(
|
||||
opts,
|
||||
moment.tz.guess()
|
||||
).build();
|
||||
|
||||
const htmlPreviews = localDateBuilder.previews.map(preview => {
|
||||
const previewNode = document.createElement("div");
|
||||
previewNode.classList.add("preview");
|
||||
if (preview.current) {
|
||||
previewNode.classList.add("current");
|
||||
}
|
||||
|
||||
const timezoneNode = document.createElement("span");
|
||||
timezoneNode.classList.add("timezone");
|
||||
timezoneNode.innerText = preview.timezone;
|
||||
previewNode.appendChild(timezoneNode);
|
||||
|
||||
const dateTimeNode = document.createElement("span");
|
||||
dateTimeNode.classList.add("date-time");
|
||||
dateTimeNode.innerText = preview.formated;
|
||||
previewNode.appendChild(dateTimeNode);
|
||||
|
||||
return previewNode;
|
||||
});
|
||||
|
||||
const previewsNode = document.createElement("div");
|
||||
previewsNode.classList.add("locale-dates-previews");
|
||||
htmlPreviews.forEach(htmlPreview =>
|
||||
previewsNode.appendChild(htmlPreview)
|
||||
);
|
||||
|
||||
this.innerHTML = DATE_TEMPLATE;
|
||||
this.setAttribute("aria-label", localDateBuilder.textPreview);
|
||||
this.dataset.htmlTooltip = previewsNode.outerHTML;
|
||||
this.classList.add("cooked-date");
|
||||
if (localDateBuilder.pastEvent) {
|
||||
this.classList.add("past");
|
||||
}
|
||||
const relativeTime = this.querySelector(".relative-time");
|
||||
relativeTime.innerText = localDateBuilder.formated;
|
||||
});
|
||||
};
|
||||
|
||||
withPluginApi("0.8.8", initializeDiscourseLocalDates);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
const { getProperties } = Ember;
|
||||
|
||||
/*
|
||||
DateWithZoneHelper provides a limited list of helpers
|
||||
to manipulate a moment object with timezones
|
||||
|
||||
- add(count unit) adds a COUNT of UNITS to a date
|
||||
- subtract(count unit) subtracts a COUNT of UNITS to a date
|
||||
- format(format) formats a date with zone in a consitent way, optional moment format
|
||||
- isDST() allows to know if a date in a specified timezone is currently under DST
|
||||
- datetimeWithZone(timezone) returns a new moment object with timezone applied
|
||||
- datetime returns the moment object
|
||||
- repetitionsBetweenDates(duration, date) return the number of repertitions of
|
||||
duration between two dates, eg for duration: "1.weeks", "2.months"...
|
||||
*/
|
||||
export default class DateWithZoneHelper {
|
||||
constructor(params = {}) {
|
||||
this.timezone = params.timezone || "UTC";
|
||||
this.localTimezone = params.localTimezone || moment.tz.guess();
|
||||
|
||||
this.datetime = moment.tz(
|
||||
getProperties(params, [
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
"hour",
|
||||
"minute",
|
||||
"second"
|
||||
]),
|
||||
this.timezone
|
||||
);
|
||||
}
|
||||
|
||||
isDST() {
|
||||
return this.datetime.tz(this.localTimezone).isDST();
|
||||
}
|
||||
|
||||
repetitionsBetweenDates(duration, date) {
|
||||
const [count, unit] = duration.split(".");
|
||||
const diff = this.datetime.diff(date, unit);
|
||||
const repetitions = diff / parseInt(count, 10);
|
||||
return Math.abs((Math.round(repetitions * 10) / 10).toFixed(1));
|
||||
}
|
||||
|
||||
add(count, unit) {
|
||||
return this._fromDatetime(
|
||||
this.datetime.clone().add(count, unit),
|
||||
this.timezone,
|
||||
this.localTimezone
|
||||
);
|
||||
}
|
||||
|
||||
subtract(count, unit) {
|
||||
return this._fromDatetime(
|
||||
this.datetime.clone().subtract(count, unit),
|
||||
this.timezone,
|
||||
this.localTimezone
|
||||
);
|
||||
}
|
||||
|
||||
datetimeWithZone(timezone) {
|
||||
return this.datetime.clone().tz(timezone);
|
||||
}
|
||||
|
||||
format(format) {
|
||||
if (format) {
|
||||
return this.datetime.tz(this.localTimezone).format(format);
|
||||
}
|
||||
|
||||
return this.datetime.tz(this.localTimezone).toISOString(true);
|
||||
}
|
||||
|
||||
static fromDatetime(datetime, timezone, localTimezone) {
|
||||
return new DateWithZoneHelper({
|
||||
year: datetime.year(),
|
||||
month: datetime.month(),
|
||||
day: datetime.date(),
|
||||
hour: datetime.hour(),
|
||||
minute: datetime.minute(),
|
||||
second: datetime.second(),
|
||||
timezone,
|
||||
localTimezone
|
||||
});
|
||||
}
|
||||
|
||||
_fromDatetime(datetime, timezone, localTimezone) {
|
||||
return DateWithZoneHelper.fromDatetime(datetime, timezone, localTimezone);
|
||||
}
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
import DateWithZoneHelper from "./date-with-zone-helper";
|
||||
|
||||
const TIME_FORMAT = "LLL";
|
||||
const DATE_FORMAT = "LL";
|
||||
const RANGE_SEPARATOR = "→";
|
||||
|
||||
export default class LocalDateBuilder {
|
||||
constructor(params = {}, localTimezone) {
|
||||
this.time = params.time;
|
||||
this.date = params.date;
|
||||
this.recurring = params.recurring;
|
||||
this.timezones = Array.from(
|
||||
new Set((params.timezones || []).filter(Boolean))
|
||||
);
|
||||
this.timezone = params.timezone || "UTC";
|
||||
this.calendar =
|
||||
typeof params.calendar === "undefined" ? true : params.calendar;
|
||||
this.displayedTimezone = params.displayedTimezone;
|
||||
this.format = params.format || (this.time ? TIME_FORMAT : DATE_FORMAT);
|
||||
this.countdown = params.countdown;
|
||||
this.localTimezone = localTimezone;
|
||||
}
|
||||
|
||||
build() {
|
||||
const [year, month, day] = this.date.split("-").map(x => parseInt(x, 10));
|
||||
const [hour, minute] = (this.time || "")
|
||||
.split(":")
|
||||
.map(x => (x ? parseInt(x, 10) : undefined));
|
||||
|
||||
let displayedTimezone;
|
||||
if (this.time) {
|
||||
displayedTimezone = this.displayedTimezone || this.localTimezone;
|
||||
} else {
|
||||
displayedTimezone =
|
||||
this.displayedTimezone || this.localTimezone || this.timezone;
|
||||
}
|
||||
|
||||
let localDate = new DateWithZoneHelper({
|
||||
year,
|
||||
month: month ? month - 1 : null,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
timezone: this.timezone,
|
||||
localTimezone: this.localTimezone
|
||||
});
|
||||
|
||||
if (this.recurring) {
|
||||
const [count, type] = this.recurring.split(".");
|
||||
|
||||
const repetitionsForType = localDate.repetitionsBetweenDates(
|
||||
this.recurring,
|
||||
moment.tz(this.localTimezone)
|
||||
);
|
||||
|
||||
localDate = localDate.add(repetitionsForType + parseInt(count, 10), type);
|
||||
}
|
||||
|
||||
const previews = this._generatePreviews(localDate, displayedTimezone);
|
||||
|
||||
return {
|
||||
pastEvent:
|
||||
!this.recurring &&
|
||||
moment.tz(this.localTimezone).isAfter(localDate.datetime),
|
||||
formated: this._applyFormatting(localDate, displayedTimezone),
|
||||
previews,
|
||||
textPreview: this._generateTextPreviews(previews)
|
||||
};
|
||||
}
|
||||
|
||||
_generateTextPreviews(previews) {
|
||||
return previews
|
||||
.map(preview => {
|
||||
const formatedZone = this._zoneWithoutPrefix(preview.timezone);
|
||||
return `${formatedZone} ${preview.formated}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
_generatePreviews(localDate, displayedTimezone) {
|
||||
const previewedTimezones = [];
|
||||
|
||||
const timezones = this.timezones.filter(
|
||||
timezone =>
|
||||
!this._isEqualZones(timezone, this.localTimezone) &&
|
||||
!this._isEqualZones(timezone, this.timezone)
|
||||
);
|
||||
|
||||
previewedTimezones.push({
|
||||
timezone: this.localTimezone,
|
||||
current: true,
|
||||
formated: this._createDateTimeRange(localDate, this.time)
|
||||
});
|
||||
|
||||
if (
|
||||
this.timezone &&
|
||||
displayedTimezone === this.localTimezone &&
|
||||
this.timezone !== displayedTimezone &&
|
||||
!this._isEqualZones(displayedTimezone, this.timezone)
|
||||
) {
|
||||
timezones.unshift(this.timezone);
|
||||
}
|
||||
|
||||
timezones.forEach(timezone => {
|
||||
if (this._isEqualZones(timezone, displayedTimezone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._isEqualZones(timezone, this.localTimezone)) {
|
||||
timezone = this.localTimezone;
|
||||
}
|
||||
|
||||
previewedTimezones.push({
|
||||
timezone,
|
||||
formated: this._createDateTimeRange(
|
||||
localDate.datetimeWithZone(timezone),
|
||||
this.time
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (!previewedTimezones.length) {
|
||||
previewedTimezones.push({
|
||||
timezone: "Etc/UTC",
|
||||
formated: this._createDateTimeRange(
|
||||
localDate.datetimeWithZone("Etc/UTC"),
|
||||
this.time
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
return previewedTimezones.uniqBy("timezone");
|
||||
}
|
||||
|
||||
_isEqualZones(timezoneA, timezoneB) {
|
||||
if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset()
|
||||
);
|
||||
}
|
||||
|
||||
_createDateTimeRange(startRange, time) {
|
||||
// if a time has been given we do not attempt to automatically create a range
|
||||
// instead we show only one date with a format showing the time
|
||||
if (time) {
|
||||
return startRange.format(TIME_FORMAT);
|
||||
} else {
|
||||
const endRange = startRange.add(24, "hours");
|
||||
return [
|
||||
startRange.format(this.format || "LLLL"),
|
||||
RANGE_SEPARATOR,
|
||||
endRange.format(this.format || "LLLL")
|
||||
].join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
_applyFormatting(localDate, displayedTimezone) {
|
||||
if (this.countdown) {
|
||||
const diffTime = moment.tz(this.localTimezone).diff(localDate.datetime);
|
||||
|
||||
if (diffTime < 0) {
|
||||
return moment.duration(diffTime).humanize();
|
||||
} else {
|
||||
return I18n.t("discourse_local_dates.relative_dates.countdown.passed");
|
||||
}
|
||||
}
|
||||
|
||||
const sameTimezone = this._isEqualZones(
|
||||
displayedTimezone,
|
||||
this.localTimezone
|
||||
);
|
||||
|
||||
if (this.calendar) {
|
||||
const inCalendarRange = moment
|
||||
.tz(this.localTimezone)
|
||||
.isBetween(
|
||||
localDate.subtract(2, "day").datetime,
|
||||
localDate.add(1, "day").datetime.endOf("day")
|
||||
);
|
||||
|
||||
if (inCalendarRange && sameTimezone) {
|
||||
return localDate.datetime.calendar(
|
||||
moment.tz(this.localTimezone),
|
||||
this._calendarFormats(this.time ? this.time : null)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sameTimezone) {
|
||||
return this._formatWithZone(localDate, displayedTimezone, this.format);
|
||||
}
|
||||
|
||||
return localDate.format(this.format);
|
||||
}
|
||||
|
||||
_calendarFormats(time) {
|
||||
return {
|
||||
sameDay: this._translateCalendarKey(time, "today"),
|
||||
nextDay: this._translateCalendarKey(time, "tomorrow"),
|
||||
lastDay: this._translateCalendarKey(time, "yesterday"),
|
||||
sameElse: "L"
|
||||
};
|
||||
}
|
||||
|
||||
_translateCalendarKey(time, key) {
|
||||
const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, {
|
||||
time: "LT"
|
||||
});
|
||||
|
||||
if (time) {
|
||||
return translated
|
||||
.split("LT")
|
||||
.map(w => `[${w}]`)
|
||||
.join("LT");
|
||||
} else {
|
||||
return `[${translated.replace(" LT", "")}]`;
|
||||
}
|
||||
}
|
||||
|
||||
_formatTimezone(timezone) {
|
||||
return timezone
|
||||
.replace("_", " ")
|
||||
.replace("Etc/", "")
|
||||
.split("/");
|
||||
}
|
||||
|
||||
_zoneWithoutPrefix(timezone) {
|
||||
const [part1, part2] = this._formatTimezone(timezone);
|
||||
return part2 || part1;
|
||||
}
|
||||
|
||||
_formatWithZone(localDate, displayedTimezone, format) {
|
||||
let formated = localDate.datetimeWithZone(displayedTimezone).format(format);
|
||||
return `${formated} (${this._zoneWithoutPrefix(displayedTimezone)})`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user