mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 14:35:27 +00:00
To make the typeahead code more readable, we extract this function to timerender. We also improve the logic to be more readable, and add tests to confirm its validity.
328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
let next_timerender_id = 0;
|
|
|
|
const set_to_start_of_day = function (time) {
|
|
return time.setMilliseconds(0).setSeconds(0).setMinutes(0).setHours(0);
|
|
};
|
|
|
|
// Given an XDate object 'time', returns an object:
|
|
// {
|
|
// time_str: a string for the current human-formatted version
|
|
// formal_time_str: a string for the current formally formatted version
|
|
// e.g. "Monday, April 15, 2017"
|
|
// needs_update: a boolean for if it will need to be updated when the
|
|
// day changes
|
|
// }
|
|
exports.render_now = function (time, today) {
|
|
const start_of_today = set_to_start_of_day(today || new XDate());
|
|
const start_of_other_day = set_to_start_of_day(time.clone());
|
|
|
|
let time_str = '';
|
|
let needs_update = false;
|
|
// render formal time to be used as title attr tooltip
|
|
// "\xa0" is U+00A0 NO-BREAK SPACE.
|
|
// Can't use as that represents the literal string " ".
|
|
const formal_time_str = time.toString('dddd,\xa0MMMM\xa0d,\xa0yyyy');
|
|
|
|
// How many days old is 'time'? 0 = today, 1 = yesterday, 7 = a
|
|
// week ago, -1 = tomorrow, etc.
|
|
|
|
// Presumably the result of diffDays will be an integer in this
|
|
// case, but round it to be sure before comparing to integer
|
|
// constants.
|
|
const days_old = Math.round(start_of_other_day.diffDays(start_of_today));
|
|
|
|
const is_older_year =
|
|
start_of_today.getFullYear() - start_of_other_day.getFullYear() > 0;
|
|
|
|
if (days_old === 0) {
|
|
time_str = i18n.t("Today");
|
|
needs_update = true;
|
|
} else if (days_old === 1) {
|
|
time_str = i18n.t("Yesterday");
|
|
needs_update = true;
|
|
} else if (is_older_year) {
|
|
// For long running servers, searching backlog can get ambiguous
|
|
// without a year stamp. Only show year if message is from an older year
|
|
time_str = time.toString("MMM\xa0dd,\xa0yyyy");
|
|
needs_update = false;
|
|
} else {
|
|
// For now, if we get a message from tomorrow, we don't bother
|
|
// rewriting the timestamp when it gets to be tomorrow.
|
|
time_str = time.toString("MMM\xa0dd");
|
|
needs_update = false;
|
|
}
|
|
return {
|
|
time_str: time_str,
|
|
formal_time_str: formal_time_str,
|
|
needs_update: needs_update,
|
|
};
|
|
};
|
|
|
|
// Current date is passed as an argument for unit testing
|
|
exports.last_seen_status_from_date = function (last_active_date, current_date) {
|
|
if (typeof current_date === 'undefined') {
|
|
current_date = new XDate();
|
|
}
|
|
|
|
const minutes = Math.floor(last_active_date.diffMinutes(current_date));
|
|
if (minutes <= 2) {
|
|
return i18n.t("Just now");
|
|
}
|
|
if (minutes < 60) {
|
|
return i18n.t("__minutes__ minutes ago", {minutes: minutes});
|
|
}
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours === 1) {
|
|
return i18n.t("An hour ago");
|
|
}
|
|
if (hours < 24) {
|
|
return i18n.t("__hours__ hours ago", {hours: hours});
|
|
}
|
|
|
|
const days = Math.floor(hours / 24);
|
|
if (days === 1) {
|
|
return i18n.t("Yesterday");
|
|
}
|
|
|
|
if (days < 90) {
|
|
return i18n.t("__days__ days ago", {days: days});
|
|
} else if (days > 90 && days < 365) {
|
|
if (current_date.getFullYear() === last_active_date.getFullYear()) {
|
|
// Online more than 90 days ago, in the same year
|
|
return i18n.t("__last_active_date__",
|
|
{last_active_date: last_active_date.toString("MMM\xa0dd")});
|
|
}
|
|
}
|
|
return i18n.t("__last_active_date__",
|
|
{last_active_date: last_active_date.toString("MMM\xa0dd,\xa0yyyy")});
|
|
};
|
|
|
|
// List of the dates that need to be updated when the day changes.
|
|
// Each timestamp is represented as a list of length 2:
|
|
// [id of the span element, XDate representing the time]
|
|
let update_list = [];
|
|
|
|
// The time at the beginning of the next day, when the timestamps are updated.
|
|
// Represented as an XDate with hour, minute, second, millisecond 0.
|
|
let next_update;
|
|
exports.initialize = function () {
|
|
next_update = set_to_start_of_day(new XDate()).addDays(1);
|
|
};
|
|
|
|
// time_above is an optional argument, to support dates that look like:
|
|
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---
|
|
function maybe_add_update_list_entry(entry) {
|
|
if (entry.needs_update) {
|
|
update_list.push(entry);
|
|
}
|
|
}
|
|
|
|
function render_date_span(elem, rendered_time, rendered_time_above) {
|
|
elem.text("");
|
|
if (rendered_time_above !== undefined) {
|
|
const pieces = [
|
|
'<i class="date-direction fa fa-caret-up"></i>',
|
|
rendered_time_above.time_str,
|
|
'<hr class="date-line">',
|
|
'<i class="date-direction fa fa-caret-down"></i>',
|
|
rendered_time.time_str,
|
|
];
|
|
elem.append(pieces);
|
|
return elem;
|
|
}
|
|
elem.append(rendered_time.time_str);
|
|
return elem.attr('title', rendered_time.formal_time_str);
|
|
}
|
|
|
|
// Given an XDate object 'time', return a DOM node that initially
|
|
// displays the human-formatted date, and is updated automatically as
|
|
// necessary (e.g. changing "Today" to "Yesterday" to "Jul 1").
|
|
// If two dates are given, it renders them as:
|
|
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---
|
|
|
|
// (What's actually spliced into the message template is the contents
|
|
// of this DOM node as HTML, so effectively a copy of the node. That's
|
|
// okay since to update the time later we look up the node by its id.)
|
|
exports.render_date = function (time, time_above, today) {
|
|
const className = "timerender" + next_timerender_id;
|
|
next_timerender_id += 1;
|
|
const rendered_time = exports.render_now(time, today);
|
|
let node = $("<span />").attr('class', className);
|
|
if (time_above !== undefined) {
|
|
const rendered_time_above = exports.render_now(time_above, today);
|
|
node = render_date_span(node, rendered_time, rendered_time_above);
|
|
} else {
|
|
node = render_date_span(node, rendered_time);
|
|
}
|
|
maybe_add_update_list_entry({
|
|
needs_update: rendered_time.needs_update,
|
|
className: className,
|
|
time: time,
|
|
time_above: time_above,
|
|
});
|
|
return node;
|
|
};
|
|
|
|
// Renders the timestamp returned by the <time:> markdown syntax.
|
|
exports.render_markdown_timestamp = function (time, now, text) {
|
|
now = now || moment();
|
|
if (page_params.timezone) {
|
|
now = now.tz(page_params.timezone);
|
|
time = time.tz(page_params.timezone);
|
|
}
|
|
const timestring = time.format('ddd, MMM D YYYY, h:mm A');
|
|
const titlestring = "This time is in your timezone. Original text was '" + text + "'.";
|
|
return {
|
|
text: timestring,
|
|
title: titlestring,
|
|
};
|
|
};
|
|
|
|
// This isn't expected to be called externally except manually for
|
|
// testing purposes.
|
|
exports.update_timestamps = function () {
|
|
const now = new XDate();
|
|
if (now >= next_update) {
|
|
const to_process = update_list;
|
|
update_list = [];
|
|
|
|
for (const entry of to_process) {
|
|
const className = entry.className;
|
|
const elements = $('.' + className);
|
|
// The element might not exist any more (because it
|
|
// was in the zfilt table, or because we added
|
|
// messages above it and re-collapsed).
|
|
if (elements !== null) {
|
|
for (const element of elements) {
|
|
const time = entry.time;
|
|
const time_above = entry.time_above;
|
|
const rendered_time = exports.render_now(time);
|
|
if (time_above) {
|
|
const rendered_time_above = exports.render_now(time_above);
|
|
render_date_span($(element), rendered_time, rendered_time_above);
|
|
} else {
|
|
render_date_span($(element), rendered_time);
|
|
}
|
|
maybe_add_update_list_entry({
|
|
needs_update: rendered_time.needs_update,
|
|
className: className,
|
|
time: time,
|
|
time_above: time_above,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
next_update = set_to_start_of_day(now.clone().addDays(1));
|
|
}
|
|
};
|
|
|
|
setInterval(exports.update_timestamps, 60 * 1000);
|
|
|
|
// Transform a Unix timestamp into a ISO 8601 formatted date string.
|
|
// Example: 1978-10-31T13:37:42Z
|
|
exports.get_full_time = function (timestamp) {
|
|
return new XDate(timestamp * 1000).toISOString();
|
|
};
|
|
|
|
exports.get_timestamp_for_flatpickr = (timestring) => {
|
|
let timestamp;
|
|
moment.suppressDeprecationWarnings = true;
|
|
try {
|
|
// If there's already a valid time in the compose box,
|
|
// we use it to initialize the flatpickr instance.
|
|
timestamp = moment(timestring);
|
|
} finally {
|
|
// Otherwise, default to showing the current time.
|
|
if (!timestamp || !timestamp.isValid()) {
|
|
timestamp = moment();
|
|
}
|
|
}
|
|
moment.suppressDeprecationWarnings = false;
|
|
return timestamp.toDate();
|
|
};
|
|
|
|
exports.stringify_time = function (time) {
|
|
if (page_params.twenty_four_hour_time) {
|
|
return time.toString('HH:mm');
|
|
}
|
|
return time.toString('h:mm TT');
|
|
};
|
|
|
|
// this is for rendering absolute time based off the preferences for twenty-four
|
|
// hour time in the format of "%mmm %d, %h:%m %p".
|
|
exports.absolute_time = (function () {
|
|
const MONTHS = [
|
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
];
|
|
|
|
const fmt_time = function (date, H_24) {
|
|
const payload = {
|
|
hours: date.getHours(),
|
|
minutes: date.getMinutes(),
|
|
};
|
|
|
|
if (payload.hours > 12 && !H_24) {
|
|
payload.hours -= 12;
|
|
payload.is_pm = true;
|
|
}
|
|
|
|
let str = ("0" + payload.hours).slice(-2) + ":" + ("0" + payload.minutes).slice(-2);
|
|
|
|
if (!H_24) {
|
|
str += payload.is_pm ? " PM" : " AM";
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
return function (timestamp, today) {
|
|
if (typeof today === 'undefined') {
|
|
today = new Date();
|
|
}
|
|
const date = new Date(timestamp);
|
|
const is_older_year = today.getFullYear() - date.getFullYear() > 0;
|
|
const H_24 = page_params.twenty_four_hour_time;
|
|
let str = MONTHS[date.getMonth()] + " " + date.getDate();
|
|
// include year if message date is from a previous year
|
|
if (is_older_year) {
|
|
str += ", " + date.getFullYear();
|
|
}
|
|
str += " " + fmt_time(date, H_24);
|
|
return str;
|
|
};
|
|
}());
|
|
|
|
exports.get_full_datetime = function (time) {
|
|
// Convert to number of hours ahead/behind UTC.
|
|
// The sign of getTimezoneOffset() is reversed wrt
|
|
// the conventional meaning of UTC+n / UTC-n
|
|
const tz_offset = -time.getTimezoneOffset() / 60;
|
|
return {
|
|
date: time.toLocaleDateString(),
|
|
time: time.toLocaleTimeString() +
|
|
' (UTC' + (tz_offset < 0 ? '' : '+') + tz_offset + ')',
|
|
};
|
|
};
|
|
|
|
// XDate.toLocaleDateString and XDate.toLocaleTimeString are
|
|
// expensive, so we delay running the following code until we need
|
|
// the full date and time strings.
|
|
exports.set_full_datetime = function timerender_set_full_datetime(message, time_elem) {
|
|
if (message.full_date_str !== undefined) {
|
|
return;
|
|
}
|
|
|
|
const time = new XDate(message.timestamp * 1000);
|
|
const full_datetime = exports.get_full_datetime(time);
|
|
|
|
message.full_date_str = full_datetime.date;
|
|
message.full_time_str = full_datetime.time;
|
|
|
|
time_elem.attr('title', message.full_date_str + ' ' + message.full_time_str);
|
|
};
|
|
|
|
window.timerender = exports;
|