discourse/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js
Natalie Tay 7d8cda9858
FEATURE: Omit showing day when 'to' day is same as 'from' day (#18500)
Essentially,

Saturday at 2:50 PM -> Saturday at 4:38 PM becomes
Saturday at 2:50 PM -> 4:38 PM (Singapore)

Also, the displayed dates are shortened when the standalone date
is within two days. So despite the 'from' and 'to' date being the
same day, it may show 'Saturday' for 'from', and the specific date
for the 'to'. This corrects the behaviour.

(so if the current date and time is Thursday 5PM, the 'from' date
below is within 2 days, but the 'to' date is not)
Saturday at 2:50 PM -> 8 October 2022 at 9:38 PM becomes
Saturday at 2:50 PM -> 9:38 PM
2022-10-07 09:39:41 +08:00

306 lines
8.4 KiB
JavaScript

import DateWithZoneHelper from "./date-with-zone-helper";
import I18n from "I18n";
import { renderIcon } from "discourse-common/lib/icon-library";
const DATETIME_FORMAT = "LLL";
const DATE_FORMAT = "LL";
const FULL_DATETIME_FORMAT = "LLLL";
const TIME_FORMAT = "h:mm A";
const DAY_OF_THE_WEEK_FORMAT = "dddd";
const RANGE_SEPARATOR = "→";
const TIME_ICON = "clock";
const SHORT_FORMAT_DAYS_PERIOD = 1;
export default class LocalDateBuilder {
constructor(params = {}, localTimezone) {
this.time = params.time;
this.date = params.date;
this.recurring = params.recurring;
this.sameLocalDayAsFrom = params.sameLocalDayAsFrom;
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 ? DATETIME_FORMAT : DATE_FORMAT);
this.countdown = params.countdown;
this.duration = params.duration;
this.localTimezone = localTimezone;
}
build() {
const [year, month, day] = this.date.split("-").map((x) => parseInt(x, 10));
const [hour, minute, second] = (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.timezone || this.localTimezone;
}
let localDate = new DateWithZoneHelper({
year,
month: month ? month - 1 : null,
day,
hour,
minute,
second,
timezone: this.timezone,
localTimezone: this.localTimezone,
});
if (this.recurring && moment().isAfter(localDate.datetime)) {
const type = this.recurring.split(".")[1];
const repetitionsForType = localDate.unitRepetitionsBetweenDates(
this.recurring,
moment.tz(this.localTimezone)
);
localDate = localDate.add(repetitionsForType, type);
}
const previews = this._generatePreviews(localDate, displayedTimezone);
return {
pastEvent:
!this.recurring &&
moment.tz(this.localTimezone).isAfter(localDate.datetime),
formatted: this._applyFormatting(localDate, displayedTimezone),
previews,
textPreview: this._generateTextPreviews(previews),
};
}
_generateTextPreviews(previews) {
return previews
.map((preview) => {
const formattedZone = this._zoneWithoutPrefix(preview.timezone);
return `${formattedZone} ${preview.formatted}`;
})
.join(", ");
}
_generatePreviews(localDate, displayedTimezone) {
const previewedTimezones = [];
const timezones = this.timezones.filter(
(timezone) => !this._isEqualZones(timezone, this.localTimezone)
);
previewedTimezones.push({
timezone: this._zoneWithoutPrefix(this.localTimezone),
current: true,
formatted: this._createDateTimeRange(
DateWithZoneHelper.fromDatetime(
localDate.datetime,
localDate.timezone,
this.localTimezone
),
this.time,
this.duration
),
});
if (
this.timezone &&
displayedTimezone === this.localTimezone &&
this.timezone !== displayedTimezone &&
!this._isEqualZones(displayedTimezone, this.timezone) &&
!this.timezones.any((t) => this._isEqualZones(t, 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: this._zoneWithoutPrefix(timezone),
formatted: this._createDateTimeRange(
DateWithZoneHelper.fromDatetime(
localDate.datetime,
localDate.timezone,
timezone
),
this.time,
this.duration
),
});
});
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, duration) {
const [startDate, endDate] = this._calculateDatesForRange(
startRange,
time,
duration
);
let formatElements = [
startDate.format(`${DAY_OF_THE_WEEK_FORMAT}, ${DATE_FORMAT}`),
this._optionalTimeIcon(startDate, endDate),
startDate.format(TIME_FORMAT),
];
if (endDate) {
formatElements = formatElements.concat([
RANGE_SEPARATOR,
endDate.format(this._endDateFormat(startDate, endDate)),
]);
}
return formatElements.filter(Boolean).join(" ");
}
_shortFormat(startDate, endDate) {
return (
endDate.datetime.diff(startDate.datetime, "days") <
SHORT_FORMAT_DAYS_PERIOD
);
}
_optionalTimeIcon(startDate, endDate) {
if (!endDate || this._shortFormat(startDate, endDate)) {
return `<br />${renderIcon("string", TIME_ICON)}`;
}
}
_endDateFormat(startDate, endDate) {
return this._shortFormat(startDate, endDate)
? TIME_FORMAT
: FULL_DATETIME_FORMAT;
}
_calculateDatesForRange(date, time, duration) {
// 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 && !duration) {
return [date];
}
const dates = [
date,
duration ? date.add(duration, "minutes") : date.add(24, "hours"),
];
return duration < 0 ? dates.reverse() : dates;
}
_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 (this.sameLocalDayAsFrom) {
return this._timeOnlyFormat(localDate, displayedTimezone);
}
if (inCalendarRange && sameTimezone) {
const date = localDate.datetimeWithZone(this.localTimezone);
if (date.hours() === 0 && date.minutes() === 0) {
return date.format("dddd");
}
return date.calendar(
moment.tz(localDate.timezone),
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 formatted = localDate
.datetimeWithZone(displayedTimezone)
.format(format);
return `${formatted} (${this._zoneWithoutPrefix(displayedTimezone)})`;
}
_timeOnlyFormat(localTime, displayedTimezone) {
return this._formatWithZone(localTime, displayedTimezone, "LT");
}
}