discourse/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js
Godfrey Chan c34f8b65cb
DEV: Rename I18n imports to discourse-i18n (#23915)
As of #23867 this is now a real package, so updating the imports to
use the real package name, rather than relying on the alias. The
name change in the package name is because `I18n` is not a valid
name as NPM packages must be all lowercase.

This commit also introduces an eslint rule to prevent importing from
the old I18n path.

For themes/plugins, the old 'i18n' name remains functional.
2023-10-18 11:07:09 +01:00

306 lines
8.4 KiB
JavaScript

import { renderIcon } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
import DateWithZoneHelper from "./date-with-zone-helper";
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);
const hasTime = hour !== undefined;
return {
pastEvent:
!this.recurring &&
moment.tz(this.localTimezone).isAfter(localDate.datetime),
formatted: this._applyFormatting(localDate, displayedTimezone, hasTime),
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, hasTime) {
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 (hasTime && 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");
}
}