discourse/plugins/discourse-local-dates/assets/javascripts/lib/local-date-builder.js
David Taylor 0ed4b09527
DEV: Move discourse-common/(utils|lib) to discourse/lib (#30733)
`discourse-common` was created in the past to share logic between the
'wizard' app and the main 'discourse' app. Since then, the wizard has
been consolidated into the main app, so the separation of
`discourse-common` is no longer useful.

This commit moves `discourse-common/(lib|utils)/*` into
`discourse/lib/*`, adds shims for the imports, and updates existing
uses in core.
2025-01-13 13:02:49 +00:00

306 lines
8.4 KiB
JavaScript

import { renderIcon } from "discourse/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("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(`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");
}
}