Repository: zulip/zulip

Issue:
Add user-definable groups
[summarizing a Zulip discussion]

Add a way for users (or, at the very least, administrators) to define their own groups, such that someone can write `@group1` to notify everyone in `group1`.

Design complication: what should happen when `group1` is (say) 100 members, but someone says `@group1` in a stream to which only 30 of those members are subscribed?  Should the sender get a prompt asking whether the other 70 people should be subscribed to the stream?



Discussion:
why not use a stream instead? whoever subscribed to it is in the group...

@ritschwumm It could be helpful to tag a specific group in a discussion that's open to a wider audience. For example, you might want to tag the support team in a discussion among all company employees.

As a workaround, you can simulate that today using the "alert words" feature -- just have all the members of a team all add `@group1` to their list of alert words.  


Resolving patch:
diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py
--- a/tools/lib/capitalization.py
+++ b/tools/lib/capitalization.py
@@ -20,6 +20,7 @@
     r"Dropbox",
     r"GitHub",
     r"Google",
+    r"Hamlet",
     r"HTTP",
     r"ID",
     r"IDs",
diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py
--- a/tools/linter_lib/custom_check.py
+++ b/tools/linter_lib/custom_check.py
@@ -503,7 +503,9 @@ def build_custom_checkers(by_lang):
          'exclude_line': [('templates/zerver/register.html', 'placeholder="acme"'),
                           ('templates/zerver/register.html', 'placeholder="Acme or Aκμή"'),
                           ('static/templates/settings/realm-domains-modal.handlebars',
-                           '<td><input type="text" class="new-realm-domain" placeholder="acme.com"></input></td>')],
+                           '<td><input type="text" class="new-realm-domain" placeholder="acme.com"></input></td>'),
+                          ("static/templates/user-groups-admin.handlebars",
+                           '<input type="text" name="name" id="user_group_name" placeholder="hamletcharacters" />')],
          'exclude': set(["static/templates/settings/emoji-settings-admin.handlebars",
                          "static/templates/settings/realm-filter-settings-admin.handlebars",
                          "static/templates/settings/bot-settings.handlebars",
diff --git a/zproject/settings.py b/zproject/settings.py
--- a/zproject/settings.py
+++ b/zproject/settings.py
@@ -1079,6 +1079,7 @@ def get_secret(key: str) -> None:
             'js/settings_streams.js',
             'js/settings_filters.js',
             'js/settings_invites.js',
+            'js/settings_user_groups.js',
             'js/settings.js',
             'js/admin_sections.js',
             'js/admin.js',


Test patch:
diff --git a/frontend_tests/node_tests/templates.js b/frontend_tests/node_tests/templates.js
--- a/frontend_tests/node_tests/templates.js
+++ b/frontend_tests/node_tests/templates.js
@@ -283,6 +283,31 @@ function render(template_name, args) {
     global.write_handlebars_output("admin_tab", html);
 }());
 
+(function admin_user_group_list() {
+    var args = {
+        user_group: {
+            id: "9",
+            name: "uranohoshi",
+            description: "Students at Uranohoshi Academy",
+        },
+    };
+
+    var html = '';
+    html += '<div id="user-groups">';
+    html += render('admin_user_group_list', args);
+    html += '</div>';
+
+    global.write_handlebars_output('admin_user_group_list', html);
+
+    var group_id = $(html).find('.user-group:first').prop('id');
+    var group_name = $(html).find('.user-group:first .pill-container').attr('data-group-pills');
+    var group_description = $(html).find('.user-group:first h4').text().trim().replace(/\s+/g, ' ');
+
+    assert.equal(group_id, '9');
+    assert.equal(group_name, 'uranohoshi');
+    assert.equal(group_description, 'uranohoshi — Students at Uranohoshi Academy');
+}());
+
 (function admin_user_list() {
     var html = '<table>';
     var users = ['alice', 'bob', 'carl'];
diff --git a/frontend_tests/node_tests/user_groups.js b/frontend_tests/node_tests/user_groups.js
--- a/frontend_tests/node_tests/user_groups.js
+++ b/frontend_tests/node_tests/user_groups.js
@@ -1,8 +1,18 @@
 set_global('blueslip', {});
+set_global('page_params', {});
 
 zrequire('user_groups');
 
 (function test_user_groups() {
+    var students = {
+        name: 'Students',
+        id: 0,
+    };
+    global.page_params.realm_user_groups = [students];
+
+    user_groups.initialize();
+    assert.equal(user_groups.get_user_group_from_id(students.id), students);
+
     var admins = {
         name: 'Admins',
         id: 1,
@@ -11,14 +21,22 @@ zrequire('user_groups');
         name: 'Everyone',
         id: 2,
     };
+
     user_groups.add(admins);
     assert.equal(user_groups.get_user_group_from_id(admins.id), admins);
+
     var called = false;
     global.blueslip.error = function (msg) {
         assert.equal(msg, "Unknown group_id in get_user_group_from_id: " + all.id);
         called = true;
     };
+
     assert.equal(user_groups.get_user_group_from_id(all.id), undefined);
     assert(called);
-}());
 
+    user_groups.remove(students);
+    global.blueslip.error = function (msg) {
+        assert.equal(msg, "Unknown group_id in get_user_group_from_id: " + students.id);
+    };
+    assert.equal(user_groups.get_user_group_from_id(students.id), undefined);
+}());