2021-02-28 21:16:38 +01:00
|
|
|
import {
|
2021-02-05 21:20:14 +01:00
|
|
|
differenceInCalendarDays,
|
2021-04-04 00:02:34 +02:00
|
|
|
differenceInMinutes,
|
2021-02-05 21:20:14 +01:00
|
|
|
format,
|
|
|
|
formatISO,
|
2021-03-04 18:23:21 +01:00
|
|
|
isEqual,
|
2021-02-05 21:20:14 +01:00
|
|
|
isValid,
|
|
|
|
parseISO,
|
|
|
|
startOfToday,
|
2021-02-28 21:16:38 +01:00
|
|
|
} from "date-fns";
|
2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
2021-04-13 08:14:20 +02:00
|
|
|
import _ from "lodash";
|
2020-07-28 00:26:58 +02:00
|
|
|
|
2021-07-01 19:45:02 +02:00
|
|
|
import render_markdown_time_tooltip from "../templates/markdown_time_tooltip.hbs";
|
|
|
|
|
2021-04-13 06:51:54 +02:00
|
|
|
import {$t} from "./i18n";
|
2021-07-28 16:00:58 +02:00
|
|
|
import {user_settings} from "./user_settings";
|
2021-03-25 21:38:40 +01:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let next_timerender_id = 0;
|
2013-02-12 23:26:25 +01:00
|
|
|
|
2021-08-03 19:50:13 +02:00
|
|
|
export function clear_for_testing(): void {
|
2021-03-14 15:57:08 +01:00
|
|
|
next_timerender_id = 0;
|
|
|
|
}
|
|
|
|
|
2021-07-02 19:13:46 +02:00
|
|
|
// Exported for tests only.
|
2021-08-03 19:50:13 +02:00
|
|
|
export function get_tz_with_UTC_offset(time: number | Date): string {
|
2021-07-02 19:13:46 +02:00
|
|
|
const tz_offset = format(time, "xxx");
|
|
|
|
let timezone = new Intl.DateTimeFormat(undefined, {timeZoneName: "short"})
|
|
|
|
.formatToParts(time)
|
2021-08-03 19:50:13 +02:00
|
|
|
.find(({type}) => type === "timeZoneName")?.value;
|
2021-07-02 19:13:46 +02:00
|
|
|
|
|
|
|
if (timezone === "UTC") {
|
|
|
|
return "UTC";
|
|
|
|
}
|
|
|
|
|
|
|
|
// When user's locale doesn't match their timezone (eg. en_US for IST),
|
|
|
|
// we get `timezone` in the format of'GMT+x:y. We don't want to
|
|
|
|
// show that along with (UTC+x:y)
|
2021-08-03 19:50:13 +02:00
|
|
|
timezone = /GMT[+-][\d:]*/.test(timezone ?? "") ? "" : timezone;
|
2021-07-02 19:13:46 +02:00
|
|
|
|
|
|
|
const tz_UTC_offset = `(UTC${tz_offset})`;
|
|
|
|
|
|
|
|
if (timezone) {
|
|
|
|
return timezone + " " + tz_UTC_offset;
|
|
|
|
}
|
|
|
|
return tz_UTC_offset;
|
|
|
|
}
|
|
|
|
|
2021-02-05 21:20:14 +01:00
|
|
|
// Given a Date object 'time', returns an object:
|
2017-05-17 23:33:47 +02:00
|
|
|
// {
|
|
|
|
// 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
|
|
|
|
// }
|
2021-08-03 19:50:13 +02:00
|
|
|
export type TimeRender = {
|
|
|
|
time_str: string;
|
|
|
|
formal_time_str: string;
|
|
|
|
needs_update: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export function render_now(time: Date, today = new Date()): TimeRender {
|
2020-07-15 01:29:15 +02:00
|
|
|
let time_str = "";
|
2019-11-02 00:06:25 +01:00
|
|
|
let needs_update = false;
|
2021-06-23 22:16:15 +02:00
|
|
|
// render formal time to be used for tippy 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 " ".
|
2021-02-05 21:20:14 +01:00
|
|
|
const formal_time_str = format(time, "EEEE,\u00A0MMMM\u00A0d,\u00A0yyyy");
|
2017-05-16 09:44:46 +02:00
|
|
|
|
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.
|
2021-02-05 21:20:14 +01:00
|
|
|
const days_old = differenceInCalendarDays(today, time);
|
2017-05-24 02:20:06 +02:00
|
|
|
|
2013-06-25 16:22:14 +02:00
|
|
|
if (days_old === 0) {
|
2021-04-13 06:51:54 +02:00
|
|
|
time_str = $t({defaultMessage: "Today"});
|
2017-05-17 23:33:47 +02:00
|
|
|
needs_update = true;
|
2013-06-25 16:22:14 +02:00
|
|
|
} else if (days_old === 1) {
|
2021-04-13 06:51:54 +02:00
|
|
|
time_str = $t({defaultMessage: "Yesterday"});
|
2017-05-17 23:33:47 +02:00
|
|
|
needs_update = true;
|
2021-02-05 21:20:14 +01:00
|
|
|
} else if (time.getFullYear() !== today.getFullYear()) {
|
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
|
2021-02-05 21:20:14 +01:00
|
|
|
time_str = format(time, "MMM\u00A0dd,\u00A0yyyy");
|
2017-05-17 23:33:47 +02:00
|
|
|
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.
|
2021-02-05 21:20:14 +01:00
|
|
|
time_str = format(time, "MMM\u00A0dd");
|
2017-05-17 23:33:47 +02:00
|
|
|
needs_update = false;
|
2013-02-12 23:26:25 +01:00
|
|
|
}
|
2017-05-17 23:33:47 +02:00
|
|
|
return {
|
2020-07-20 22:18:43 +02:00
|
|
|
time_str,
|
|
|
|
formal_time_str,
|
|
|
|
needs_update,
|
2017-05-17 23:33:47 +02:00
|
|
|
};
|
2021-02-28 01:14:36 +01: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
|
2021-08-03 19:50:13 +02:00
|
|
|
export function last_seen_status_from_date(
|
|
|
|
last_active_date: Date,
|
|
|
|
current_date = new Date(),
|
|
|
|
): string {
|
2021-02-05 21:20:14 +01:00
|
|
|
const minutes = differenceInMinutes(current_date, last_active_date);
|
2017-05-12 20:16:39 +02:00
|
|
|
if (minutes <= 2) {
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t({defaultMessage: "Just now"});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
|
|
|
if (minutes < 60) {
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t({defaultMessage: "{minutes} minutes ago"}, {minutes});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
2021-02-05 21:20:14 +01:00
|
|
|
|
|
|
|
const days_old = differenceInCalendarDays(current_date, last_active_date);
|
2019-11-02 00:06:25 +01:00
|
|
|
const hours = Math.floor(minutes / 60);
|
2020-10-02 11:46:25 +02:00
|
|
|
|
2021-06-17 23:07:35 +02:00
|
|
|
if (hours < 24) {
|
2020-10-02 11:46:25 +02:00
|
|
|
if (hours === 1) {
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t({defaultMessage: "An hour ago"});
|
2020-10-02 11:46:25 +02:00
|
|
|
}
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t({defaultMessage: "{hours} hours ago"}, {hours});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
|
|
|
|
2020-10-02 11:46:25 +02:00
|
|
|
if (days_old === 1) {
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t({defaultMessage: "Yesterday"});
|
2017-05-12 20:16:39 +02:00
|
|
|
}
|
|
|
|
|
2020-10-02 11:46:25 +02:00
|
|
|
if (days_old < 90) {
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t({defaultMessage: "{days_old} days ago"}, {days_old});
|
2021-02-05 21:20:14 +01:00
|
|
|
} else if (
|
|
|
|
days_old > 90 &&
|
|
|
|
days_old < 365 &&
|
|
|
|
last_active_date.getFullYear() === current_date.getFullYear()
|
|
|
|
) {
|
2020-12-22 11:26:39 +01:00
|
|
|
// Online more than 90 days ago, in the same year
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t(
|
|
|
|
{defaultMessage: "{last_active_date}"},
|
|
|
|
{last_active_date: format(last_active_date, "MMM\u00A0dd")},
|
|
|
|
);
|
2019-03-13 19:23:57 +01:00
|
|
|
}
|
2021-04-13 06:51:54 +02:00
|
|
|
return $t(
|
|
|
|
{defaultMessage: "{last_active_date}"},
|
|
|
|
{last_active_date: format(last_active_date, "MMM\u00A0dd,\u00A0yyyy")},
|
|
|
|
);
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
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:
|
2021-02-05 21:20:14 +01:00
|
|
|
// [id of the span element, Date representing the time]
|
2021-08-03 19:50:13 +02:00
|
|
|
type UpdateEntry = {
|
|
|
|
needs_update: boolean;
|
|
|
|
className: string;
|
|
|
|
time: Date;
|
|
|
|
time_above?: Date;
|
|
|
|
};
|
|
|
|
let update_list: UpdateEntry[] = [];
|
2013-02-12 23:26:25 +01:00
|
|
|
|
2021-02-05 21:20:14 +01:00
|
|
|
// The time at the beginning of the day, when the timestamps were updated.
|
|
|
|
// Represented as a Date with hour, minute, second, millisecond 0.
|
2021-08-03 19:50:13 +02:00
|
|
|
let last_update: Date;
|
2021-02-28 01:14:36 +01:00
|
|
|
|
2021-08-03 19:50:13 +02:00
|
|
|
export function initialize(): void {
|
2021-02-05 21:20:14 +01:00
|
|
|
last_update = startOfToday();
|
2021-02-28 01:14:36 +01: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 ▼ ---
|
2021-08-03 19:50:13 +02:00
|
|
|
function maybe_add_update_list_entry(entry: UpdateEntry): void {
|
2017-06-03 20:23:43 +02:00
|
|
|
if (entry.needs_update) {
|
|
|
|
update_list.push(entry);
|
2013-06-25 16:22:14 +02:00
|
|
|
}
|
2013-02-12 23:26:25 +01:00
|
|
|
}
|
|
|
|
|
2021-08-03 19:50:13 +02:00
|
|
|
function render_date_span(
|
|
|
|
elem: JQuery,
|
|
|
|
rendered_time: TimeRender,
|
|
|
|
rendered_time_above?: TimeRender,
|
|
|
|
): JQuery {
|
2013-08-29 01:34:10 +02:00
|
|
|
elem.text("");
|
2017-05-17 23:33:47 +02:00
|
|
|
if (rendered_time_above !== undefined) {
|
2021-08-03 19:50:13 +02:00
|
|
|
elem.append(
|
2018-10-15 15:52:54 +02:00
|
|
|
'<i class="date-direction fa fa-caret-up"></i>',
|
2021-04-13 08:14:20 +02:00
|
|
|
_.escape(rendered_time_above.time_str),
|
2017-06-02 16:50:23 +02:00
|
|
|
'<hr class="date-line">',
|
2018-10-15 15:52:54 +02:00
|
|
|
'<i class="date-direction fa fa-caret-down"></i>',
|
2021-04-13 08:14:20 +02:00
|
|
|
_.escape(rendered_time.time_str),
|
2021-08-03 19:50:13 +02:00
|
|
|
);
|
2017-06-02 16:50:23 +02:00
|
|
|
return elem;
|
2013-08-29 01:34:10 +02:00
|
|
|
}
|
2021-04-13 08:14:20 +02:00
|
|
|
elem.append(_.escape(rendered_time.time_str));
|
2021-06-23 22:16:15 +02:00
|
|
|
return elem.attr("data-tippy-content", rendered_time.formal_time_str);
|
2013-08-29 01:34:10 +02:00
|
|
|
}
|
|
|
|
|
2021-02-05 21:20:14 +01:00
|
|
|
// Given an Date 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.)
|
2021-08-03 19:50:13 +02:00
|
|
|
export function render_date(time: Date, time_above: Date | undefined, today: Date): JQuery {
|
2019-11-02 00:06:25 +01:00
|
|
|
const className = "timerender" + next_timerender_id;
|
2016-11-30 19:05:04 +01:00
|
|
|
next_timerender_id += 1;
|
2021-02-28 01:14:36 +01:00
|
|
|
const rendered_time = render_now(time, today);
|
2020-07-15 01:29:15 +02:00
|
|
|
let node = $("<span />").attr("class", className);
|
2013-08-29 01:34:10 +02:00
|
|
|
if (time_above !== undefined) {
|
2021-02-28 01:14:36 +01:00
|
|
|
const rendered_time_above = 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({
|
2018-05-06 21:43:17 +02:00
|
|
|
needs_update: rendered_time.needs_update,
|
2020-07-20 22:18:43 +02:00
|
|
|
className,
|
|
|
|
time,
|
|
|
|
time_above,
|
2017-06-03 20:23:43 +02:00
|
|
|
});
|
2013-02-12 23:26:25 +01:00
|
|
|
return node;
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
2013-01-14 17:26:50 +01:00
|
|
|
|
2020-08-11 01:47:49 +02:00
|
|
|
// Renders the timestamp returned by the <time:> Markdown syntax.
|
2021-08-03 19:50:13 +02:00
|
|
|
export function render_markdown_timestamp(time: number | Date): {
|
|
|
|
text: string;
|
|
|
|
tooltip_content: string;
|
|
|
|
} {
|
2021-07-28 16:00:58 +02:00
|
|
|
const hourformat = user_settings.twenty_four_hour_time ? "HH:mm" : "h:mm a";
|
2020-09-29 22:20:46 +02:00
|
|
|
const timestring = format(time, "E, MMM d yyyy, " + hourformat);
|
2021-07-01 19:45:02 +02:00
|
|
|
|
|
|
|
const tz_offset_str = get_tz_with_UTC_offset(time);
|
|
|
|
const tooltip_html_content = render_markdown_time_tooltip({tz_offset_str});
|
|
|
|
|
2018-02-13 00:33:36 +01:00
|
|
|
return {
|
|
|
|
text: timestring,
|
2021-07-01 19:45:02 +02:00
|
|
|
tooltip_content: tooltip_html_content,
|
2018-02-13 00:33:36 +01:00
|
|
|
};
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
2018-02-13 00:33:36 +01:00
|
|
|
|
2013-02-26 20:44:44 +01:00
|
|
|
// This isn't expected to be called externally except manually for
|
|
|
|
// testing purposes.
|
2021-08-03 19:50:13 +02:00
|
|
|
export function update_timestamps(): void {
|
2021-02-05 21:20:14 +01:00
|
|
|
const today = startOfToday();
|
2021-03-04 18:23:21 +01:00
|
|
|
if (!isEqual(today, last_update)) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const to_process = update_list;
|
2013-06-25 16:22:14 +02:00
|
|
|
update_list = [];
|
|
|
|
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const entry of to_process) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const className = entry.className;
|
2021-02-03 23:23:32 +01:00
|
|
|
const elements = $(`.${CSS.escape(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).
|
2021-03-04 18:07:43 +01:00
|
|
|
if (elements.length > 0) {
|
|
|
|
const time = entry.time;
|
|
|
|
const time_above = entry.time_above;
|
|
|
|
const rendered_time = render_now(time, today);
|
|
|
|
const rendered_time_above = time_above ? render_now(time_above, today) : undefined;
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const element of elements) {
|
2021-03-04 18:07:43 +01:00
|
|
|
render_date_span($(element), rendered_time, rendered_time_above);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2021-03-04 18:07:43 +01:00
|
|
|
maybe_add_update_list_entry({
|
|
|
|
needs_update: rendered_time.needs_update,
|
|
|
|
className,
|
|
|
|
time,
|
|
|
|
time_above,
|
|
|
|
});
|
2013-06-25 16:22:14 +02:00
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2013-06-25 16:22:14 +02:00
|
|
|
|
2021-02-05 21:20:14 +01:00
|
|
|
last_update = today;
|
2013-02-12 23:26:25 +01:00
|
|
|
}
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
2013-02-26 20:44:44 +01:00
|
|
|
|
2021-02-28 01:14:36 +01:00
|
|
|
setInterval(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
|
2021-08-03 19:50:13 +02:00
|
|
|
export function get_full_time(timestamp: number): string {
|
2021-02-05 21:20:14 +01:00
|
|
|
return formatISO(timestamp * 1000);
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
2017-02-20 00:14:52 +01:00
|
|
|
|
2021-08-03 19:50:13 +02:00
|
|
|
export function get_timestamp_for_flatpickr(timestring: string): Date {
|
2020-07-06 19:09:29 +02:00
|
|
|
let timestamp;
|
|
|
|
try {
|
|
|
|
// If there's already a valid time in the compose box,
|
|
|
|
// we use it to initialize the flatpickr instance.
|
2020-09-29 22:20:46 +02:00
|
|
|
timestamp = parseISO(timestring);
|
2020-07-06 19:09:29 +02:00
|
|
|
} finally {
|
|
|
|
// Otherwise, default to showing the current time.
|
2020-09-29 22:20:46 +02:00
|
|
|
if (!timestamp || !isValid(timestamp)) {
|
|
|
|
timestamp = new Date();
|
2020-07-06 19:09:29 +02:00
|
|
|
}
|
|
|
|
}
|
2020-09-29 22:20:46 +02:00
|
|
|
return timestamp;
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
2020-07-06 19:09:29 +02:00
|
|
|
|
2021-08-03 19:50:13 +02:00
|
|
|
export function stringify_time(time: number | Date): string {
|
2021-07-28 16:00:58 +02:00
|
|
|
if (user_settings.twenty_four_hour_time) {
|
2021-02-05 21:20:14 +01:00
|
|
|
return format(time, "HH:mm");
|
2017-12-25 21:43:06 +01:00
|
|
|
}
|
2021-02-05 21:20:14 +01:00
|
|
|
return format(time, "h:mm a");
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|
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".
|
2021-09-22 23:28:19 +02:00
|
|
|
export function absolute_time(timestamp: number, today = new Date()): string {
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
const is_older_year = today.getFullYear() - date.getFullYear() > 0;
|
|
|
|
const H_24 = user_settings.twenty_four_hour_time;
|
|
|
|
|
|
|
|
return format(
|
|
|
|
date,
|
|
|
|
is_older_year
|
|
|
|
? H_24
|
|
|
|
? "MMM d, yyyy HH:mm"
|
|
|
|
: "MMM d, yyyy hh:mm a"
|
|
|
|
: H_24
|
|
|
|
? "MMM d HH:mm"
|
|
|
|
: "MMM d hh:mm a",
|
|
|
|
);
|
|
|
|
}
|
2017-02-24 02:30:47 +01:00
|
|
|
|
2021-08-03 19:50:13 +02:00
|
|
|
export function get_full_datetime(time: Date): string {
|
|
|
|
const time_options: Intl.DateTimeFormatOptions = {timeStyle: "medium"};
|
2021-06-12 22:10:34 +02:00
|
|
|
|
2021-07-28 16:00:58 +02:00
|
|
|
if (user_settings.twenty_four_hour_time) {
|
2021-06-27 19:16:36 +02:00
|
|
|
time_options.hourCycle = "h24";
|
2021-06-12 22:10:34 +02:00
|
|
|
}
|
|
|
|
|
2021-06-27 19:16:36 +02:00
|
|
|
const date_string = time.toLocaleDateString();
|
|
|
|
let time_string = time.toLocaleTimeString(undefined, time_options);
|
2021-06-12 22:10:34 +02:00
|
|
|
|
2021-07-02 19:13:46 +02:00
|
|
|
const tz_offset_str = get_tz_with_UTC_offset(time);
|
2021-06-27 19:16:36 +02:00
|
|
|
|
2021-07-02 19:13:46 +02:00
|
|
|
time_string = time_string + " " + tz_offset_str;
|
2021-06-27 19:16:36 +02:00
|
|
|
|
|
|
|
return $t({defaultMessage: "{date} at {time}"}, {date: date_string, time: time_string});
|
2021-02-28 01:14:36 +01:00
|
|
|
}
|