diff --git a/frontend_tests/node_tests/people.js b/frontend_tests/node_tests/people.js index 6bda83c0dd..ce73fb2433 100644 --- a/frontend_tests/node_tests/people.js +++ b/frontend_tests/node_tests/people.js @@ -2,21 +2,18 @@ const {strict: assert} = require("assert"); +const {parseISO} = require("date-fns"); const _ = require("lodash"); -const moment = require("moment-timezone"); -const rewiremock = require("rewiremock/node"); +const MockDate = require("mockdate"); const {set_global, zrequire} = require("../zjsunit/namespace"); const {run_test} = require("../zjsunit/test"); -const people = rewiremock.proxy(() => zrequire("people"), { - "moment-timezone": () => moment("20130208T080910"), -}); - set_global("message_store", {}); set_global("page_params", {}); set_global("settings_data", {}); +const people = zrequire("people"); const settings_config = zrequire("settings_config"); const visibility = settings_config.email_address_visibility_values; const admins_only = visibility.admins_only.code; @@ -28,6 +25,8 @@ function set_email_visibility(code) { set_email_visibility(admins_only); +MockDate.set(parseISO("20130208T080910").getTime()); + const welcome_bot = { email: "welcome-bot@example.com", user_id: 4, @@ -399,7 +398,7 @@ run_test("user_timezone", () => { page_params.twenty_four_hour_time = true; assert.deepEqual(people.get_user_time_preferences(me.user_id), expected_pref); - expected_pref.format = "h:mm A"; + expected_pref.format = "h:mm a"; page_params.twenty_four_hour_time = false; assert.deepEqual(people.get_user_time_preferences(me.user_id), expected_pref); @@ -1111,3 +1110,6 @@ run_test("get_active_message_people", () => { active_message_people = people.get_active_message_people(); assert.deepEqual(active_message_people, [steven, maria]); }); + +// reset to native Date() +MockDate.reset(); diff --git a/frontend_tests/node_tests/timerender.js b/frontend_tests/node_tests/timerender.js index 6da3fe8a0d..10d4a838d1 100644 --- a/frontend_tests/node_tests/timerender.js +++ b/frontend_tests/node_tests/timerender.js @@ -2,7 +2,7 @@ const {strict: assert} = require("assert"); -const moment = require("moment"); +const {getTime} = require("date-fns"); const XDate = require("xdate"); const {set_global, zrequire} = require("../zjsunit/namespace"); @@ -150,10 +150,10 @@ run_test("get_timestamp_for_flatpickr", () => { Date.now = () => new Date("2020-07-07T10:00:00Z").getTime(); // Invalid timestamps should show current time. - assert.equal(func("random str").valueOf(), moment().valueOf()); + assert.equal(func("random str").valueOf(), getTime(new Date())); // Valid ISO timestamps should return Date objects. - assert.equal(func(iso_timestamp).valueOf(), moment(unix_timestamp).valueOf()); + assert.equal(func(iso_timestamp).valueOf(), getTime(new Date(unix_timestamp))); // Restore the Date object. Date.now = date_now; diff --git a/package.json b/package.json index 2448a0d44b..d6f848e841 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "core-js": "^3.6.5", "css-loader": "^5.0.0", "css.escape": "^1.5.1", + "date-fns": "^2.16.1", + "date-fns-tz": "^1.1.1", "emoji-datasource-google": "^6.0.0", "emoji-datasource-google-blob": "npm:emoji-datasource-google@^3.0.0", "emoji-datasource-twitter": "^6.0.0", @@ -45,8 +47,6 @@ "katex": "^0.12.0", "lodash": "^4.17.19", "mini-css-extract-plugin": "^1.2.0", - "moment": "^2.24.0", - "moment-timezone": "^0.5.25", "optimize-css-assets-webpack-plugin": "^5.0.3", "plotly.js": "^1.48.1", "postcss": "^8.0.3", @@ -98,6 +98,7 @@ "eslint-plugin-import": "^2.22.0", "js-yaml": "^4.0.0", "jsdom": "^16.1.0", + "mockdate": "^3.0.2", "nyc": "^15.0.0", "openapi-examples-validator": "^4.0.0", "prettier": "^2.0.5", diff --git a/static/.gitignore b/static/.gitignore index 6d3aa5432a..589ab283f3 100644 --- a/static/.gitignore +++ b/static/.gitignore @@ -10,5 +10,7 @@ /generated/emoji-styles # From passing pygments data to the frontend /generated/pygments_data.json +# From passing timezones data to the frontend +/generated/timezones.json # Legacy emoji data directory /third/emoji-data diff --git a/static/js/composebox_typeahead.js b/static/js/composebox_typeahead.js index 921207066c..6714405650 100644 --- a/static/js/composebox_typeahead.js +++ b/static/js/composebox_typeahead.js @@ -1,9 +1,9 @@ "use strict"; const autosize = require("autosize"); +const {formatISO} = require("date-fns"); const ConfirmDatePlugin = require("flatpickr/dist/plugins/confirmDate/confirmDate"); const _ = require("lodash"); -const moment = require("moment"); const pygments_data = require("../generated/pygments_data.json"); const emoji = require("../shared/js/emoji"); @@ -754,10 +754,7 @@ const show_flatpickr = (element, callback, default_timestamp) => { plugins: [new ConfirmDatePlugin({})], positionElement: element, dateFormat: "Z", - formatDate: (date) => { - const dt = moment(date); - return dt.local().format(); - }, + formatDate: (date) => formatISO(date), }); const container = $($(instance.innerContainer).parent()); container.on("click", ".flatpickr-calendar", (e) => { diff --git a/static/js/markdown.js b/static/js/markdown.js index 495a95d30c..8311887378 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -1,8 +1,8 @@ "use strict"; +const {isValid} = require("date-fns"); const katex = require("katex"); const _ = require("lodash"); -const moment = require("moment"); const emoji = require("../shared/js/emoji"); const fenced_code = require("../shared/js/fenced_code"); @@ -294,18 +294,14 @@ function handleEmoji(emoji_name) { function handleTimestamp(time) { let timeobject; if (Number.isNaN(Number(time))) { - // Moment throws a large deprecation warning when it has to fallback - // to the Date() constructor. We needn't worry here and can let backend - // Markdown handle any dates that moment misses. - moment.suppressDeprecationWarnings = true; - timeobject = moment(time); // not a Unix timestamp + timeobject = new Date(time); // not a Unix timestamp } else { // JavaScript dates are in milliseconds, Unix timestamps are in seconds - timeobject = moment(time * 1000); + timeobject = new Date(time * 1000); } const escaped_time = _.escape(time); - if (timeobject === null || !timeobject.isValid()) { + if (timeobject === null || !isValid(timeobject)) { // Unsupported time format: rerender accordingly. // We do not show an error on these formats in local echo because diff --git a/static/js/people.js b/static/js/people.js index 7b6a5dd10a..c9722a5fb1 100644 --- a/static/js/people.js +++ b/static/js/people.js @@ -1,6 +1,6 @@ import md5 from "blueimp-md5"; +import {format, utcToZonedTime} from "date-fns-tz"; import _ from "lodash"; -import moment from "moment-timezone"; import * as typeahead from "../shared/js/typeahead"; @@ -252,7 +252,8 @@ export function get_user_time_preferences(user_id) { export function get_user_time(user_id) { const user_pref = get_user_time_preferences(user_id); if (user_pref) { - return moment().tz(user_pref.timezone).format(user_pref.format); + const current_date = utcToZonedTime(new Date(), user_pref.timezone); + return format(current_date, user_pref.format, {timeZone: user_pref.timezone}); } return undefined; } diff --git a/static/js/popovers.js b/static/js/popovers.js index b817989343..0a8fcef043 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -1,8 +1,8 @@ "use strict"; const ClipboardJS = require("clipboard"); +const {parseISO, formatISO, add, set} = require("date-fns"); const ConfirmDatePlugin = require("flatpickr/dist/plugins/confirmDate/confirmDate"); -const moment = require("moment"); const render_actions_popover_content = require("../templates/actions_popover_content.hbs"); const render_mobile_message_buttons_popover = require("../templates/mobile_message_buttons_popover.hbs"); @@ -143,7 +143,7 @@ function get_custom_profile_field_data(user, field, field_types, dateFormat) { switch (field_type) { case field_types.DATE.id: - profile_field.value = moment(field_value.value).format(dateFormat); + profile_field.value = dateFormat.format(parseISO(field_value.value)); break; case field_types.USER.id: profile_field.id = field.id; @@ -328,7 +328,7 @@ exports.hide_user_profile = function () { exports.show_user_profile = function (user) { exports.hide_all(); - const dateFormat = moment.localeData().longDateFormat("LL"); + const dateFormat = new Intl.DateTimeFormat("default", {dateStyle: "long"}); const field_types = page_params.custom_profile_field_types; const profile_data = page_params.custom_profile_fields .map((f) => get_custom_profile_field_data(user, f, field_types, dateFormat)) @@ -340,7 +340,7 @@ exports.show_user_profile = function (user) { profile_data, user_avatar: "avatar/" + user.email + "/medium", is_me: people.is_current_user(user.email), - date_joined: moment(user.date_joined).format(dateFormat), + date_joined: dateFormat.format(parseISO(user.date_joined)), last_seen: buddy_data.user_last_seen_time_status(user.user_id), show_email: settings_data.show_email(), user_time: people.get_user_time(user.user_id), @@ -584,7 +584,7 @@ exports.render_actions_remind_popover = function (element, id) { ).flatpickr({ enableTime: true, clickOpens: false, - defaultDate: moment().format(), + defaultDate: "today", minDate: "today", plugins: [new ConfirmDatePlugin({})], }); @@ -1106,27 +1106,31 @@ exports.register_click_handlers = function () { } $("body").on("click", ".remind.in_20m", (e) => { - const datestr = moment().add(20, "m").format(); + const datestr = formatISO(add(new Date(), {minutes: 20})); reminder_click_handler(datestr, e); }); $("body").on("click", ".remind.in_1h", (e) => { - const datestr = moment().add(1, "h").format(); + const datestr = formatISO(add(new Date(), {hours: 1})); reminder_click_handler(datestr, e); }); $("body").on("click", ".remind.in_3h", (e) => { - const datestr = moment().add(3, "h").format(); + const datestr = formatISO(add(new Date(), {hours: 3})); reminder_click_handler(datestr, e); }); $("body").on("click", ".remind.tomo", (e) => { - const datestr = moment().add(1, "d").hour(9).minute(0).seconds(0).format(); + const datestr = formatISO( + set(add(new Date(), {days: 1}), {hours: 9, minutes: 0, seconds: 0}), + ); reminder_click_handler(datestr, e); }); $("body").on("click", ".remind.nxtw", (e) => { - const datestr = moment().add(1, "w").day("monday").hour(9).minute(0).seconds(0).format(); + const datestr = formatISO( + set(add(new Date(), {weeks: 1}), {hours: 9, minutes: 0, seconds: 0}), + ); reminder_click_handler(datestr, e); }); diff --git a/static/js/portico/signup.js b/static/js/portico/signup.js index 592b22a524..a01c6ccf36 100644 --- a/static/js/portico/signup.js +++ b/static/js/portico/signup.js @@ -1,7 +1,5 @@ "use strict"; -const moment = require("moment-timezone"); - $(() => { // NB: this file is included on multiple pages. In each context, // some of the jQuery selectors below will return empty lists. @@ -82,7 +80,7 @@ $(() => { $(".team_subdomain_error_server").css("display", "none"); } - $("#timezone").val(moment.tz.guess()); + $("#timezone").val(new Intl.DateTimeFormat().resolvedOptions().timeZone); } // Code in this block will be executed when the /accounts/send_confirm diff --git a/static/js/reminder.js b/static/js/reminder.js index cf578e1c6f..18f5f62f8d 100644 --- a/static/js/reminder.js +++ b/static/js/reminder.js @@ -1,7 +1,5 @@ "use strict"; -const moment = require("moment-timezone"); - const people = require("./people"); const util = require("./util"); @@ -37,7 +35,7 @@ function patch_request_for_scheduling(request, message_content, deliver_at, deli new_request.content = message_content; new_request.deliver_at = deliver_at; new_request.delivery_type = delivery_type; - new_request.tz_guess = moment.tz.guess(); + new_request.tz_guess = new Intl.DateTimeFormat().resolvedOptions().timeZone; return new_request; } diff --git a/static/js/rendered_markdown.js b/static/js/rendered_markdown.js index 6e17ddd378..ed9302ed91 100644 --- a/static/js/rendered_markdown.js +++ b/static/js/rendered_markdown.js @@ -1,7 +1,7 @@ "use strict"; const ClipboardJS = require("clipboard"); -const moment = require("moment"); +const {parseISO, isValid} = require("date-fns"); const copy_code_button = require("../templates/copy_code_button.hbs"); const view_code_in_playground = require("../templates/view_code_in_playground.hbs"); @@ -155,20 +155,15 @@ exports.update_elements = (content) => { return; } - // Moment throws a large deprecation warning when it has to - // fallback to the Date() constructor. This isn't really a - // problem for us except in local echo, as the backend always - // uses a format that ensures that is unnecessary. - moment.suppressDeprecationWarnings = true; - const timestamp = moment(time_str); - if (timestamp.isValid()) { + const timestamp = parseISO(time_str); + if (isValid(timestamp)) { const text = $(this).text(); const rendered_time = timerender.render_markdown_timestamp(timestamp, text); $(this).text(rendered_time.text); $(this).attr("title", rendered_time.title); } else { // This shouldn't happen. If it does, we're very interested in debugging it. - blueslip.error(`Moment could not parse datetime supplied by backend: ${time_str}`); + blueslip.error(`Could not parse datetime supplied by backend: ${time_str}`); } }); diff --git a/static/js/settings.js b/static/js/settings.js index ac1a653bfb..8db7ade66d 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1,7 +1,6 @@ "use strict"; -const moment = require("moment-timezone"); - +const timezones = require("../generated/timezones.json"); const render_settings_tab = require("../templates/settings_tab.hbs"); const people = require("./people"); @@ -71,7 +70,7 @@ exports.build_page = function () { page_params.enable_sounds || page_params.enable_stream_audible_notifications, zuliprc: "zuliprc", botserverrc: "botserverrc", - timezones: moment.tz.names(), + timezones: timezones.timezones, can_create_new_bots: settings_bots.can_create_new_bots(), settings_label: exports.settings_label, demote_inactive_streams_values: settings_config.demote_inactive_streams_values, diff --git a/static/js/settings_data.js b/static/js/settings_data.js index cad2bf9979..6e94f153e8 100644 --- a/static/js/settings_data.js +++ b/static/js/settings_data.js @@ -54,7 +54,7 @@ exports.get_time_preferences = function (user_timezone) { } return { timezone: user_timezone, - format: "h:mm A", + format: "h:mm a", }; }; diff --git a/static/js/timerender.js b/static/js/timerender.js index 7447bfb718..19fbb6720f 100644 --- a/static/js/timerender.js +++ b/static/js/timerender.js @@ -1,6 +1,6 @@ "use strict"; -const moment = require("moment"); +const {format, parseISO, isValid} = require("date-fns"); const XDate = require("xdate"); let next_timerender_id = 0; @@ -174,8 +174,8 @@ exports.render_date = function (time, time_above, today) { // Renders the timestamp returned by the Markdown syntax. exports.render_markdown_timestamp = function (time, text) { - const hourformat = page_params.twenty_four_hour_time ? "HH:mm" : "h:mm A"; - const timestring = time.format("ddd, MMM D YYYY, " + hourformat); + const hourformat = page_params.twenty_four_hour_time ? "HH:mm" : "h:mm a"; + const timestring = format(time, "E, MMM d yyyy, " + hourformat); const titlestring = "This time is in your timezone. Original text was '" + text + "'."; return { text: timestring, @@ -232,19 +232,17 @@ exports.get_full_time = function (timestamp) { 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); + timestamp = parseISO(timestring); } finally { // Otherwise, default to showing the current time. - if (!timestamp || !timestamp.isValid()) { - timestamp = moment(); + if (!timestamp || !isValid(timestamp)) { + timestamp = new Date(); } } - moment.suppressDeprecationWarnings = false; - return timestamp.toDate(); + return timestamp; }; exports.stringify_time = function (time) { diff --git a/tools/lib/provision_inner.py b/tools/lib/provision_inner.py index d79ab40b19..e2e6c5d353 100755 --- a/tools/lib/provision_inner.py +++ b/tools/lib/provision_inner.py @@ -10,6 +10,7 @@ ZULIP_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__f sys.path.append(ZULIP_PATH) from pygments import __version__ as pygments_version +from pytz import VERSION as timezones_version from scripts.lib.zulip_tools import ( ENDC, @@ -47,6 +48,12 @@ def build_pygments_data_paths() -> List[str]: ] return paths +def build_timezones_data_paths() -> List[str]: + paths = [ + "tools/setup/build_timezone_values", + ] + return paths + def compilemessages_paths() -> List[str]: paths = ['zerver/management/commands/compilemessages.py'] paths += glob.glob('locale/*/LC_MESSAGES/*.po') @@ -135,6 +142,16 @@ def need_to_run_build_pygments_data() -> bool: [pygments_version], ) +def need_to_run_build_timezone_data() -> bool: + if not os.path.exists("static/generated/timezones.json"): + return True + + return is_digest_obsolete( + "build_timezones_data_hash", + build_timezones_data_paths(), + [timezones_version], + ) + def need_to_run_compilemessages() -> bool: if not os.path.exists('locale/language_name_map.json'): # User may have cleaned their git checkout. @@ -213,6 +230,16 @@ def main(options: argparse.Namespace) -> int: else: print("No need to run `tools/setup/build_pygments_data`.") + if options.is_force or need_to_run_build_timezone_data(): + run(["tools/setup/build_timezone_values"]) + write_new_digest( + "build_timezones_data_hash", + build_timezones_data_paths(), + [timezones_version], + ) + else: + print("No need to run `tools/setup/build_timezone_values`.") + if options.is_force or need_to_run_inline_email_css(): run(["scripts/setup/inline_email_css.py"]) write_new_digest( diff --git a/tools/setup/build_timezone_values b/tools/setup/build_timezone_values new file mode 100755 index 0000000000..51c0f4f055 --- /dev/null +++ b/tools/setup/build_timezone_values @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import json +import os + +import pytz + +ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../') +OUT_PATH = os.path.join(ZULIP_PATH, 'static', 'generated', 'timezones.json') + +with open(OUT_PATH, 'w') as f: + json.dump({"timezones": pytz.all_timezones}, f) diff --git a/tools/update-prod-static b/tools/update-prod-static index 42218d8ae4..4d354fb68c 100755 --- a/tools/update-prod-static +++ b/tools/update-prod-static @@ -44,6 +44,9 @@ run(['./tools/setup/generate_zulip_bots_static_files.py']) # Build pygment data run(['./tools/setup/build_pygments_data']) +# Build timezones data +run(['./tools/setup/build_timezone_values']) + # Create webpack bundle run(['./tools/webpack', '--quiet']) diff --git a/version.py b/version.py index 7ef81e9a40..01d958b104 100644 --- a/version.py +++ b/version.py @@ -43,4 +43,4 @@ API_FEATURE_LEVEL = 38 # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = '124.2' +PROVISION_VERSION = '125.0' diff --git a/yarn.lock b/yarn.lock index cc9b2596df..33ee395eeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3865,6 +3865,16 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-fns-tz@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.1.1.tgz#2e0dfcc62cc5b7b5fa7ea620f11a5e7f63a7ed75" + integrity sha512-5PR604TlyvpiNXtvn+PZCcCazsI8fI1am3/aimNFN8CMqHQ0KRl+6hB46y4mDbB7bk3+caEx3qHhS7Ewac/FIg== + +date-fns@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" + integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -8191,17 +8201,10 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment-timezone@^0.5.25: - version "0.5.32" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2" - integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0", moment@^2.24.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== +mockdate@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.2.tgz#a5a7bb5820da617747af424d7a4dcb22c6c03d79" + integrity sha512-ldfYSUW1ocqSHGTK6rrODUiqAFPGAg0xaHqYJ5tvj1hQyFsjuHpuWgWFTZWwDVlzougN/s2/mhDr8r5nY5xDpA== monotone-convex-hull-2d@^1.0.1: version "1.0.1"