Files
zulip/static/js/timerender.js
Rohitt Vashishtha 648307ef33 rendered_markdown: Add rendering functions for timestamps.
This code generates the timestamp string to be shown to the user
from the given timestamp in unix format using moment.js.

We also render the timestamp in a pill.
2020-05-21 12:37:37 -07:00

303 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.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;
};
}());
// 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);
// 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;
message.full_date_str = time.toLocaleDateString();
message.full_time_str = time.toLocaleTimeString() +
' (UTC' + (tz_offset < 0 ? '' : '+') + tz_offset + ')';
time_elem.attr('title', message.full_date_str + ' ' + message.full_time_str);
};
window.timerender = exports;