2013-01-14 17:26:50 +01:00
|
|
|
var timerender = (function () {
|
|
|
|
|
|
|
|
var exports = {};
|
|
|
|
|
2013-02-12 23:26:25 +01:00
|
|
|
var next_timerender_id = 0;
|
|
|
|
|
|
|
|
var set_to_start_of_day = function (time) {
|
|
|
|
return time.setMilliseconds(0).setSeconds(0).setMinutes(0).setHours(0);
|
|
|
|
};
|
|
|
|
|
2017-05-17 23:33:47 +02:00
|
|
|
// 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
|
|
|
|
// }
|
2017-05-18 21:18:11 +02:00
|
|
|
exports.render_now = function (time, today) {
|
|
|
|
var start_of_today = set_to_start_of_day(today || new XDate());
|
2013-02-12 23:26:25 +01:00
|
|
|
var start_of_other_day = set_to_start_of_day(time.clone());
|
|
|
|
|
2017-05-17 23:33:47 +02:00
|
|
|
var time_str = '';
|
|
|
|
var needs_update = false;
|
2017-05-16 09:44:46 +02:00
|
|
|
// render formal time to be used as title attr tooltip
|
2017-05-17 23:33:47 +02:00
|
|
|
// "\xa0" is U+00A0 NO-BREAK SPACE.
|
|
|
|
// Can't use as that represents the literal string " ".
|
2017-05-16 09:44:46 +02:00
|
|
|
var formal_time_str = time.toString('dddd,\xa0MMMM\xa0d,\xa0yyyy');
|
|
|
|
|
2013-02-12 23:26:25 +01:00
|
|
|
// 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
|
2013-02-26 20:32:49 +01:00
|
|
|
// case, but round it to be sure before comparing to integer
|
|
|
|
// constants.
|
2013-02-12 23:26:25 +01:00
|
|
|
var days_old = Math.round(start_of_other_day.diffDays(start_of_today));
|
|
|
|
|
2017-05-24 02:20:06 +02:00
|
|
|
var is_older_year =
|
|
|
|
(start_of_today.getFullYear() - start_of_other_day.getFullYear()) > 0;
|
|
|
|
|
2013-06-25 16:22:14 +02:00
|
|
|
if (days_old === 0) {
|
2017-05-17 23:33:47 +02:00
|
|
|
time_str = i18n.t("Today");
|
|
|
|
needs_update = true;
|
2013-06-25 16:22:14 +02:00
|
|
|
} else if (days_old === 1) {
|
2017-05-17 23:33:47 +02:00
|
|
|
time_str = i18n.t("Yesterday");
|
|
|
|
needs_update = true;
|
2017-05-24 02:20:06 +02:00
|
|
|
} else if (is_older_year) {
|
2016-07-13 00:43:19 +02:00
|
|
|
// For long running servers, searching backlog can get ambiguous
|
2017-05-24 02:20:06 +02:00
|
|
|
// without a year stamp. Only show year if message is from an older year
|
2017-05-17 23:33:47 +02:00
|
|
|
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;
|
2013-02-12 23:26:25 +01:00
|
|
|
}
|
2017-05-17 23:33:47 +02:00
|
|
|
return {
|
|
|
|
time_str: time_str,
|
|
|
|
formal_time_str: formal_time_str,
|
|
|
|
needs_update: needs_update,
|
|
|
|
};
|
2016-07-26 20:35:03 +02:00
|
|
|
};
|
2013-02-12 23:26:25 +01:00
|
|
|
|
2017-05-12 20:16:39 +02:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
var minutes = Math.floor(last_active_date.diffMinutes(current_date));
|
|
|
|
if (minutes <= 2) {
|
|
|
|
return i18n.t("Last seen just now");
|
|
|
|
}
|
|
|
|
if (minutes < 60) {
|
2017-09-05 09:00:32 +02:00
|
|
|
return i18n.t("Last seen __minutes__ minutes ago", {minutes: minutes});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var hours = Math.floor(minutes / 60);
|
|
|
|
if (hours === 1) {
|
|
|
|
return i18n.t("Last seen an hour ago");
|
|
|
|
}
|
|
|
|
if (hours < 24) {
|
2017-09-05 09:00:32 +02:00
|
|
|
return i18n.t("Last seen __hours__ hours ago", {hours: hours});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var days = Math.floor(hours / 24);
|
|
|
|
if (days === 1) {
|
|
|
|
return [i18n.t("Last seen yesterday")];
|
|
|
|
}
|
|
|
|
if (days < 365) {
|
2017-09-05 09:00:32 +02:00
|
|
|
return i18n.t("Last seen on __last_active__",
|
|
|
|
{last_active: last_active_date.toString("MMM\xa0dd")});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
|
|
|
|
2017-09-05 09:00:32 +02:00
|
|
|
return i18n.t("Last seen on __last_active_date__",
|
|
|
|
{last_active_date: last_active_date.toString("MMM\xa0dd,\xa0yyyy")});
|
2017-05-12 20:16:39 +02:00
|
|
|
};
|
|
|
|
|
2013-06-25 16:22:14 +02:00
|
|
|
// List of the dates that need to be updated when the day changes.
|
2013-02-12 23:26:25 +01:00
|
|
|
// Each timestamp is represented as a list of length 2:
|
|
|
|
// [id of the span element, XDate representing the time]
|
2013-06-25 16:22:14 +02:00
|
|
|
var update_list = [];
|
2013-02-12 23:26:25 +01:00
|
|
|
|
2013-06-25 16:22:14 +02:00
|
|
|
// The time at the beginning of the next day, when the timestamps are updated.
|
2013-02-12 23:26:25 +01:00
|
|
|
// Represented as an XDate with hour, minute, second, millisecond 0.
|
2013-06-25 16:22:14 +02:00
|
|
|
var next_update;
|
2013-05-16 22:47:53 +02:00
|
|
|
$(function () {
|
2017-02-25 21:02:13 +01:00
|
|
|
next_update = set_to_start_of_day(new XDate()).addDays(1);
|
2013-05-16 22:47:53 +02:00
|
|
|
});
|
2013-02-12 23:26:25 +01:00
|
|
|
|
2013-08-29 01:34:10 +02:00
|
|
|
// time_above is an optional argument, to support dates that look like:
|
|
|
|
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---
|
2017-06-03 20:23:43 +02:00
|
|
|
function maybe_add_update_list_entry(entry) {
|
|
|
|
if (entry.needs_update) {
|
|
|
|
update_list.push(entry);
|
2013-06-25 16:22:14 +02:00
|
|
|
}
|
2013-02-12 23:26:25 +01:00
|
|
|
}
|
|
|
|
|
2017-05-17 23:33:47 +02:00
|
|
|
function render_date_span(elem, rendered_time, rendered_time_above) {
|
2013-08-29 01:34:10 +02:00
|
|
|
elem.text("");
|
2017-05-17 23:33:47 +02:00
|
|
|
if (rendered_time_above !== undefined) {
|
2017-06-02 16:50:23 +02:00
|
|
|
var pieces = [
|
|
|
|
'<i class="date-direction icon-vector-caret-up"></i>',
|
|
|
|
rendered_time_above.time_str,
|
|
|
|
'<hr class="date-line">',
|
|
|
|
'<i class="date-direction icon-vector-caret-down"></i>',
|
|
|
|
rendered_time.time_str,
|
|
|
|
];
|
|
|
|
elem.append(pieces);
|
|
|
|
return elem;
|
2013-08-29 01:34:10 +02:00
|
|
|
}
|
2017-06-02 16:50:23 +02:00
|
|
|
elem.append(rendered_time.time_str);
|
|
|
|
return elem.attr('title', rendered_time.formal_time_str);
|
2013-08-29 01:34:10 +02:00
|
|
|
}
|
|
|
|
|
2013-01-14 17:26:50 +01:00
|
|
|
// Given an XDate object 'time', return a DOM node that initially
|
2013-07-01 20:51:39 +02:00
|
|
|
// displays the human-formatted date, and is updated automatically as
|
|
|
|
// necessary (e.g. changing "Today" to "Yesterday" to "Jul 1").
|
2013-08-29 01:34:10 +02:00
|
|
|
// If two dates are given, it renders them as:
|
|
|
|
// --- ▲ Yesterday ▲ ------ ▼ Today ▼ ---
|
2013-01-14 17:26:50 +01:00
|
|
|
|
2013-02-12 23:26:25 +01:00
|
|
|
// (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.)
|
2017-05-18 21:18:11 +02:00
|
|
|
exports.render_date = function (time, time_above, today) {
|
2017-06-03 20:10:09 +02:00
|
|
|
var className = "timerender" + next_timerender_id;
|
2016-11-30 19:05:04 +01:00
|
|
|
next_timerender_id += 1;
|
2017-05-18 21:18:11 +02:00
|
|
|
var rendered_time = exports.render_now(time, today);
|
2017-06-03 20:10:09 +02:00
|
|
|
var node = $("<span />").attr('class', className);
|
2013-08-29 01:34:10 +02:00
|
|
|
if (time_above !== undefined) {
|
2017-05-18 21:18:11 +02:00
|
|
|
var rendered_time_above = exports.render_now(time_above, today);
|
2017-05-17 23:33:47 +02:00
|
|
|
node = render_date_span(node, rendered_time, rendered_time_above);
|
2013-08-29 01:34:10 +02:00
|
|
|
} else {
|
2017-05-17 23:33:47 +02:00
|
|
|
node = render_date_span(node, rendered_time);
|
2013-08-29 01:34:10 +02:00
|
|
|
}
|
2017-06-03 20:23:43 +02:00
|
|
|
maybe_add_update_list_entry({
|
|
|
|
needs_update: rendered_time.needs_update,
|
|
|
|
className: className,
|
|
|
|
time: time,
|
|
|
|
time_above: time_above,
|
|
|
|
});
|
2013-02-12 23:26:25 +01:00
|
|
|
return node;
|
2013-01-14 17:26:50 +01:00
|
|
|
};
|
|
|
|
|
2013-02-26 20:44:44 +01:00
|
|
|
// This isn't expected to be called externally except manually for
|
|
|
|
// testing purposes.
|
|
|
|
exports.update_timestamps = function () {
|
2017-06-03 20:10:09 +02:00
|
|
|
var now = new XDate();
|
|
|
|
if (now >= next_update) {
|
2013-06-25 16:22:14 +02:00
|
|
|
var to_process = update_list;
|
|
|
|
update_list = [];
|
|
|
|
|
2017-06-03 20:10:09 +02:00
|
|
|
_.each(to_process, function (entry) {
|
2017-06-03 20:23:43 +02:00
|
|
|
var className = entry.className;
|
2017-06-03 20:10:09 +02:00
|
|
|
var elements = $('.' + className);
|
2013-06-25 16:22:14 +02:00
|
|
|
// The element might not exist any more (because it
|
|
|
|
// was in the zfilt table, or because we added
|
|
|
|
// messages above it and re-collapsed).
|
2017-06-03 20:10:09 +02:00
|
|
|
if (elements !== null) {
|
|
|
|
_.each(elements, function (element) {
|
2017-06-03 20:23:43 +02:00
|
|
|
var time = entry.time;
|
|
|
|
var time_above = entry.time_above;
|
2017-06-03 20:10:09 +02:00
|
|
|
var rendered_time = exports.render_now(time);
|
2017-06-03 20:23:43 +02:00
|
|
|
if (time_above) {
|
2017-06-03 20:10:09 +02:00
|
|
|
var rendered_time_above = exports.render_now(time_above);
|
|
|
|
render_date_span($(element), rendered_time, rendered_time_above);
|
|
|
|
} else {
|
|
|
|
render_date_span($(element), rendered_time);
|
|
|
|
}
|
2017-06-03 20:23:43 +02:00
|
|
|
maybe_add_update_list_entry({
|
|
|
|
needs_update: rendered_time.needs_update,
|
|
|
|
className: className,
|
|
|
|
time: time,
|
|
|
|
time_above: time_above,
|
|
|
|
});
|
2017-06-03 20:10:09 +02:00
|
|
|
});
|
2013-06-25 16:22:14 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-06-03 20:10:09 +02:00
|
|
|
next_update = set_to_start_of_day(now.clone().addDays(1));
|
2013-02-12 23:26:25 +01:00
|
|
|
}
|
2013-02-26 20:44:44 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
setInterval(exports.update_timestamps, 60 * 1000);
|
2013-02-12 23:26:25 +01:00
|
|
|
|
2017-06-21 21:37:53 +02:00
|
|
|
// Transform a Unix timestamp into a ISO 8601 formatted date string.
|
|
|
|
// Example: 1978-10-31T13:37:42Z
|
2017-02-20 00:14:52 +01:00
|
|
|
exports.get_full_time = function (timestamp) {
|
2017-06-21 21:37:53 +02:00
|
|
|
return new XDate(timestamp * 1000).toISOString();
|
2017-02-20 00:14:52 +01:00
|
|
|
};
|
|
|
|
|
2017-12-25 21:43:06 +01:00
|
|
|
exports.stringify_time = function (time) {
|
|
|
|
if (page_params.twenty_four_hour_time) {
|
|
|
|
return time.toString('HH:mm');
|
|
|
|
}
|
|
|
|
return time.toString('h:mm TT');
|
|
|
|
};
|
2017-02-24 02:30:47 +01:00
|
|
|
|
|
|
|
// 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 () {
|
2017-12-08 14:24:32 +01:00
|
|
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
|
|
|
2017-02-24 02:30:47 +01:00
|
|
|
var fmt_time = function (date, H_24) {
|
|
|
|
var payload = {
|
|
|
|
hours: date.getHours(),
|
|
|
|
minutes: date.getMinutes(),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (payload.hours > 12 && !H_24) {
|
|
|
|
payload.hours -= 12;
|
|
|
|
payload.is_pm = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
var str = ("0" + payload.hours).slice(-2) + ":" + ("0" + payload.minutes).slice(-2);
|
|
|
|
|
|
|
|
if (!H_24) {
|
|
|
|
str += payload.is_pm ? " PM" : " AM";
|
|
|
|
}
|
|
|
|
|
|
|
|
return str;
|
|
|
|
};
|
|
|
|
|
2017-07-26 07:20:31 +02:00
|
|
|
return function (timestamp, today) {
|
|
|
|
if (typeof today === 'undefined') {
|
|
|
|
today = new Date();
|
|
|
|
}
|
2017-02-24 02:30:47 +01:00
|
|
|
var date = new Date(timestamp);
|
2017-07-26 07:20:31 +02:00
|
|
|
var is_older_year = (today.getFullYear() - date.getFullYear()) > 0;
|
2017-02-24 02:30:47 +01:00
|
|
|
var H_24 = page_params.twenty_four_hour_time;
|
2017-07-26 07:20:31 +02:00
|
|
|
var 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;
|
2017-02-24 02:30:47 +01:00
|
|
|
};
|
|
|
|
}());
|
|
|
|
|
2013-03-05 00:18:04 +01:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
var 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
|
|
|
|
var 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);
|
|
|
|
};
|
|
|
|
|
2013-01-14 17:26:50 +01:00
|
|
|
return exports;
|
|
|
|
}());
|
2016-12-04 08:59:56 +01:00
|
|
|
|
|
|
|
if (typeof module !== 'undefined') {
|
|
|
|
module.exports = timerender;
|
|
|
|
}
|