2021-02-28 21:16:38 +01:00
|
|
|
import {
|
2021-02-05 21:20:14 +01:00
|
|
|
differenceInCalendarDays,
|
recent_topics: Standardize format of last message time.
We follow how other apps present older messages, e.g. Gmail,
Facebook Messenger, etc. display it.
Specifically, the logic we use is:
If the time is <24hr ago, show an absolute time, like "21:30" (or "9:30pm").
Otherwise, show what day it was, and not a time
If the day was yesterday, say "Yesterday".
Otherwise, if it was <7 days ago, say the day of week, like "Friday".
Otherwise, if it was <1 year ago, say the month and day, like "Sep 6".
Otherwise, say the year, month, and day, like "Sep 9, 2020".
With some tweaks from Tim Abbott to better handle the future case.
Fixes #19775
2022-02-09 14:59:51 +01:00
|
|
|
differenceInHours,
|
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";
|
2022-03-02 23:06:33 +01:00
|
|
|
import {parse_html} from "./ui_util";
|
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";
|
|
|
|
}
|
|
|
|
|
2022-02-24 21:15:43 +01:00
|
|
|
// When user's locale doesn't match their time zone (eg. en_US for IST),
|
2021-07-02 19:13:46 +02:00
|
|
|
// 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(
|
2022-01-25 11:36:19 +01:00
|
|
|
$elem: JQuery,
|
2021-08-03 19:50:13 +02:00
|
|
|
rendered_time: TimeRender,
|
|
|
|
rendered_time_above?: TimeRender,
|
|
|
|
): JQuery {
|
2022-01-25 11:36:19 +01:00
|
|
|
$elem.text("");
|
2017-05-17 23:33:47 +02:00
|
|
|
if (rendered_time_above !== undefined) {
|
2022-01-25 11:36:19 +01: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
|
|
|
);
|
2022-01-25 11:36:19 +01:00
|
|
|
return $elem;
|
2013-08-29 01:34:10 +02:00
|
|
|
}
|
2022-01-25 11:36:19 +01:00
|
|
|
$elem.append(_.escape(rendered_time.time_str));
|
|
|
|
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 {
|
2021-09-22 23:34:58 +02: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);
|
2022-01-25 11:36:19 +01: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);
|
2022-01-25 11:36:19 +01:00
|
|
|
$node = render_date_span($node, rendered_time, rendered_time_above);
|
2013-08-29 01:34:10 +02:00
|
|
|
} else {
|
2022-01-25 11:36:19 +01: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
|
|
|
});
|
2022-01-25 11:36:19 +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.
|
2022-03-02 23:06:33 +01:00
|
|
|
export function format_markdown_time(time: number | Date): string {
|
2021-07-28 16:00:58 +02:00
|
|
|
const hourformat = user_settings.twenty_four_hour_time ? "HH:mm" : "h:mm a";
|
2022-03-02 23:06:33 +01:00
|
|
|
return format(time, "E, MMM d yyyy, " + hourformat);
|
|
|
|
}
|
2021-07-01 19:45:02 +02:00
|
|
|
|
2022-03-02 23:06:33 +01:00
|
|
|
export function get_markdown_time_tooltip(reference: HTMLElement): DocumentFragment | string {
|
|
|
|
if (reference instanceof HTMLTimeElement) {
|
|
|
|
const time = parseISO(reference.dateTime);
|
|
|
|
const tz_offset_str = get_tz_with_UTC_offset(time);
|
|
|
|
return parse_html(render_markdown_time_tooltip({tz_offset_str}));
|
|
|
|
}
|
|
|
|
return "";
|
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;
|
2022-01-25 11:36:19 +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).
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($elements.length > 0) {
|
2021-03-04 18:07:43 +01:00
|
|
|
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;
|
2022-01-25 11:36:19 +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
|
|
|
|
recent_topics: Standardize format of last message time.
We follow how other apps present older messages, e.g. Gmail,
Facebook Messenger, etc. display it.
Specifically, the logic we use is:
If the time is <24hr ago, show an absolute time, like "21:30" (or "9:30pm").
Otherwise, show what day it was, and not a time
If the day was yesterday, say "Yesterday".
Otherwise, if it was <7 days ago, say the day of week, like "Friday".
Otherwise, if it was <1 year ago, say the month and day, like "Sep 6".
Otherwise, say the year, month, and day, like "Sep 9, 2020".
With some tweaks from Tim Abbott to better handle the future case.
Fixes #19775
2022-02-09 14:59:51 +01:00
|
|
|
export function format_time_modern(time: number | Date, today = new Date()): String {
|
|
|
|
const hours = differenceInHours(today, time);
|
|
|
|
const days_old = differenceInCalendarDays(today, time);
|
|
|
|
|
|
|
|
if (time > today) {
|
|
|
|
/* For timestamps in the future, we always show the year*/
|
|
|
|
return format(time, "MMM\u00A0dd,\u00A0yyyy");
|
|
|
|
} else if (hours < 24) {
|
|
|
|
return stringify_time(time);
|
|
|
|
} else if (days_old === 1) {
|
|
|
|
return $t({defaultMessage: "Yesterday"});
|
|
|
|
} else if (days_old < 7) {
|
|
|
|
return format(time, "EEEE");
|
|
|
|
} else if (days_old <= 180) {
|
|
|
|
return format(time, "MMM\u00A0dd");
|
|
|
|
}
|
|
|
|
|
|
|
|
return format(time, "MMM\u00A0dd,\u00A0yyyy");
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|