Repository: zulip/zulip

Issue:
Replace moment.js with a smaller library with frontend timezone support
We don't use `moment.js` very much, and half the callpoints are hacks to suppress warnings.  `moment` is also very large, and that's not going to change (https://momentjs.com/docs/#/-project-status/) as the project is in long-term maintenance mode.  So we should look at replacing it with a more modern/smaller library.

```
tabbott@coset:~/zulip$ git grep moment[.]
static/js/markdown.js:        moment.suppressDeprecationWarnings = true;
static/js/popovers.js:    const dateFormat = moment.localeData().longDateFormat("LL");
static/js/portico/signup.js:        $("#timezone").val(moment.tz.guess());
static/js/reminder.js:    new_request.tz_guess = moment.tz.guess();
static/js/rendered_markdown.js:        moment.suppressDeprecationWarnings = true;
static/js/settings.js:        timezones: moment.tz.names(),
static/js/timerender.js:    moment.suppressDeprecationWarnings = true;
static/js/timerender.js:    moment.suppressDeprecationWarnings = false;
templates/analytics/realm_summary_table.html:    $('#utctime')[0].innerHTML = moment.utc(now).format('YYYY-MM-DD HH:mm') + 'Z';
```

Tagging as a priority because this is one of our larger JavaScript dependencies.


Discussion:
Hello @zulip/server-dependencies members, this issue was labeled with the "area: dependencies" label, so you may want to check it out!

<!-- areaLabelAddition -->
https://github.com/you-dont-need/You-Dont-Need-Momentjs#readme may be of use.
@zulipbot claim

Hey , I am stuck in an error during resolving it and eventually created a topic : https://chat.zulip.org/#narrow/stream/49-development-help/topic/Not.20a.20function.20error
Would love to know my error :)
https://zulip.readthedocs.io/en/latest/subsystems/dependencies.html may be helpful.
We probably also want to replace XDate, which also looks unmaintained, and will almost certainly be redundant with whatever replaces Moment.js.
Oh Cool , Will also look into it . As of now , I am trying to replace <code><a href = 'https://momentjs.com/'>moment.js</a></code> with <code><a href = 'https://date-fns.org/'>date-fns</a></code> . (As , date-fns is having a <code>parsed-size</code> size of 11.5 kb , while that of moment.js is 217 kb)

Resolving patch:
diff --git a/tools/lib/provision_inner.py b/tools/lib/provision_inner.py
--- a/tools/lib/provision_inner.py
+++ b/tools/lib/provision_inner.py
@@ -10,6 +10,7 @@
 
 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/version.py b/version.py
--- a/version.py
+++ b/version.py
@@ -43,4 +43,4 @@
 #   historical commits sharing the same major version, in which case a
 #   minor version bump suffices.
 
-PROVISION_VERSION = '124.2'
+PROVISION_VERSION = '125.0'


Test patch:
diff --git a/frontend_tests/node_tests/people.js b/frontend_tests/node_tests/people.js
--- 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
--- 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;