Repository: zulip/zulip

Issue:
Update and improve direct message search filters
As [discussed on CZO](https://chat.zulip.org/#narrow/stream/101-design/topic/.22Private.22.20vs.20.22Direct.22/near/1516241), following up on #24224, we should make the following changes to the search filters related to direct messages.

Note that links should be rewritten with the new URLs.

## is:private
- [ ] Rename `is:private` to `is:dm`.
- [ ] Typing `is:private` into the search box should be automatically substituted with `is:dm`.
- [ ] Change the URL `/is/private` -> `/is/dm`.

## pm-with
- [ ] Rename to `dm:`.
- [ ]  Ideally, typing `pm-with:` into the search box should be automatically substituted with `dm:`.
- [ ] Change the URL `/pm-with/` -> `/dm/`

## group-pm-with

- [ ] Drop support for `group-pm-with`.
- [ ] Instead, add a `dm-including` filter to search for all DM conversations including a user (including 1:1 and group DMs).
- [ ] Rename this option "Group direct messages including" -> "Direct messages including" in the typeahead.
- [ ] The new URL will be `/dm-including`. The old filter can be remapped to a combination of two filters: `/dm-including/user/-dm/user`.


The documentation on https://zulip.com/help/search-for-messages should be updated to reflect these changes.

----

Update:

The URL changes are blocked for now, but we can proceed on the other parts of this issue. Blocker:

- [ ]  https://github.com/zulip/zulip-mobile/issues/5692


Discussion:
The URLs are something mobile (and perhaps terminal?) interprets in order to be able to follow links. So before we start generating different URLs (and causing people to put them in messages), we should update mobile to understand the new URLs, and get a release out with that update.

I think it'd be OK to go ahead with the other changes, while holding off on the URL changes. It means the terms in the search bar will temporarily not match the URL as closely as they usually do; but probably most users don't notice the URL that much anyway.
> I think it'd be OK to go ahead with the other changes, while holding off on the URL changes.

Got it, that plan sounds good to me.
Should I file a corresponding mobile issue?

Also @neiljp for whether we need to do some work on the ZT side as well.
Please do, thanks!
@zulipbot claim
@UjjwalAggarwal-1 This issue cannot be claimed, as someone else is already working on it. Please see our [contributor guide](https://zulip.readthedocs.io/en/latest/overview/contributing.html#your-first-codebase-contribution) for advice on finding an issue to work on. Thanks!


Resolving patch:
diff --git a/version.py b/version.py
--- a/version.py
+++ b/version.py
@@ -33,7 +33,7 @@
 # Changes should be accompanied by documentation explaining what the
 # new level means in api_docs/changelog.md, as well as "**Changes**"
 # entries in the endpoint's documentation in `zulip.yaml`.
-API_FEATURE_LEVEL = 176
+API_FEATURE_LEVEL = 177
 
 # Bump the minor PROVISION_VERSION to indicate that folks should provision
 # only when going from an old version of the code to a newer version. Bump
diff --git a/zerver/lib/narrow.py b/zerver/lib/narrow.py
--- a/zerver/lib/narrow.py
+++ b/zerver/lib/narrow.py
@@ -12,6 +12,7 @@
     Mapping,
     Optional,
     Sequence,
+    Set,
     Tuple,
     TypeVar,
     Union,
@@ -133,7 +134,7 @@ def is_web_public_narrow(narrow: Optional[Iterable[Dict[str, Any]]]) -> bool:
 
 def build_narrow_filter(narrow: Collection[Sequence[str]]) -> Callable[[Mapping[str, Any]], bool]:
     """Changes to this function should come with corresponding changes to
-    BuildNarrowFilterTest."""
+    NarrowLibraryTest."""
     check_supported_events_narrow_filter(narrow)
 
     def narrow_filter(event: Mapping[str, Any]) -> bool:
@@ -156,7 +157,8 @@ def narrow_filter(event: Mapping[str, Any]) -> bool:
             elif operator == "sender":
                 if operand.lower() != message["sender_email"].lower():
                     return False
-            elif operator == "is" and operand == "private":
+            elif operator == "is" and operand in ["dm", "private"]:
+                # "is:private" is a legacy alias for "is:dm"
                 if message["type"] != "private":
                     return False
             elif operator == "is" and operand in ["starred"]:
@@ -267,14 +269,18 @@ def __init__(
             "near": self.by_near,
             "id": self.by_id,
             "search": self.by_search,
-            "pm-with": self.by_pm_with,
+            "dm": self.by_dm,
+            # "pm-with:" is a legacy alias for "dm:"
+            "pm-with": self.by_dm,
+            "dm-including": self.by_dm_including,
+            # "group-pm-with:" was deprecated by the addition of "dm-including:"
             "group-pm-with": self.by_group_pm_with,
             # TODO/compatibility: Prior to commit a9b3a9c, the server implementation
             # for documented search operators with dashes, also implicitly supported
             # clients sending those same operators with underscores. We can remove
             # support for the below operators when support for the associated dashed
             # operator is removed.
-            "pm_with": self.by_pm_with,
+            "pm_with": self.by_dm,
             "group_pm_with": self.by_group_pm_with,
         }
 
@@ -336,7 +342,8 @@ def by_is(self, query: Select, operand: str, maybe_negate: ConditionTransform) -
         assert not self.is_web_public_query
         assert self.user_profile is not None
 
-        if operand == "private":
+        if operand in ["dm", "private"]:
+            # "is:private" is a legacy alias for "is:dm"
             cond = column("flags", Integer).op("&")(UserMessage.flags.is_private.mask) != 0
             return query.where(maybe_negate(cond))
         elif operand == "starred":
@@ -506,7 +513,7 @@ def by_id(
         cond = self.msg_id_column == literal(operand)
         return query.where(maybe_negate(cond))
 
-    def by_pm_with(
+    def by_dm(
         self, query: Select, operand: Union[str, Iterable[int]], maybe_negate: ConditionTransform
     ) -> Select:
         # This operator does not support is_web_public_query.
@@ -540,22 +547,22 @@ def by_pm_with(
         except (JsonableError, ValidationError):
             raise BadNarrowOperatorError("unknown user in " + str(operand))
 
-        # Group DM
+        # Group direct message
         if recipient.type == Recipient.HUDDLE:
             cond = column("recipient_id", Integer) == recipient.id
             return query.where(maybe_negate(cond))
 
-        # 1:1 PM
+        # 1:1 direct message
         other_participant = None
 
-        # Find if another person is in PM
+        # Find if another person is in direct message
         for user in user_profiles:
             if user.id != self.user_profile.id:
                 other_participant = user
 
-        # PM with another person
+        # Direct message with another person
         if other_participant:
-            # We need bidirectional messages PM with another person.
+            # We need bidirectional direct messages with another person.
             # But Recipient.PERSONAL objects only encode the person who
             # received the message, and not the other participant in
             # the thread (the sender), we need to do a somewhat
@@ -574,13 +581,74 @@ def by_pm_with(
             )
             return query.where(maybe_negate(cond))
 
-        # PM with self
+        # Direct message with self
         cond = and_(
             column("sender_id", Integer) == self.user_profile.id,
             column("recipient_id", Integer) == recipient.id,
         )
         return query.where(maybe_negate(cond))
 
+    def _get_huddle_recipients(self, other_user: UserProfile) -> Set[int]:
+        self_recipient_ids = [
+            recipient_tuple["recipient_id"]
+            for recipient_tuple in Subscription.objects.filter(
+                user_profile=self.user_profile,
+                recipient__type=Recipient.HUDDLE,
+            ).values("recipient_id")
+        ]
+        narrow_recipient_ids = [
+            recipient_tuple["recipient_id"]
+            for recipient_tuple in Subscription.objects.filter(
+                user_profile=other_user,
+                recipient__type=Recipient.HUDDLE,
+            ).values("recipient_id")
+        ]
+
+        return set(self_recipient_ids) & set(narrow_recipient_ids)
+
+    def by_dm_including(
+        self, query: Select, operand: Union[str, int], maybe_negate: ConditionTransform
+    ) -> Select:
+        # This operator does not support is_web_public_query.
+        assert not self.is_web_public_query
+        assert self.user_profile is not None
+
+        try:
+            if isinstance(operand, str):
+                narrow_user_profile = get_user_including_cross_realm(operand, self.realm)
+            else:
+                narrow_user_profile = get_user_by_id_in_realm_including_cross_realm(
+                    operand, self.realm
+                )
+        except UserProfile.DoesNotExist:
+            raise BadNarrowOperatorError("unknown user " + str(operand))
+
+        # "dm-including" when combined with the user's own ID/email as the operand
+        # should return all group and 1:1 direct messages (including direct messages
+        # with self), so the simplest query to get these messages is the same as "is:dm".
+        if narrow_user_profile.id == self.user_profile.id:
+            cond = column("flags", Integer).op("&")(UserMessage.flags.is_private.mask) != 0
+            return query.where(maybe_negate(cond))
+
+        # all direct messages including another person (group and 1:1)
+        huddle_recipient_ids = self._get_huddle_recipients(narrow_user_profile)
+
+        self_recipient_id = self.user_profile.recipient_id
+        # See note above in `by_dm` about needing bidirectional messages
+        # for direct messages with another person.
+        cond = or_(
+            and_(
+                column("sender_id", Integer) == narrow_user_profile.id,
+                column("recipient_id", Integer) == self_recipient_id,
+            ),
+            and_(
+                column("sender_id", Integer) == self.user_profile.id,
+                column("recipient_id", Integer) == narrow_user_profile.recipient_id,
+            ),
+            column("recipient_id", Integer).in_(huddle_recipient_ids),
+        )
+        return query.where(maybe_negate(cond))
+
     def by_group_pm_with(
         self, query: Select, operand: Union[str, int], maybe_negate: ConditionTransform
     ) -> Select:
@@ -596,22 +664,7 @@ def by_group_pm_with(
         except UserProfile.DoesNotExist:
             raise BadNarrowOperatorError("unknown user " + str(operand))
 
-        self_recipient_ids = [
-            recipient_tuple["recipient_id"]
-            for recipient_tuple in Subscription.objects.filter(
-                user_profile=self.user_profile,
-                recipient__type=Recipient.HUDDLE,
-            ).values("recipient_id")
-        ]
-        narrow_recipient_ids = [
-            recipient_tuple["recipient_id"]
-            for recipient_tuple in Subscription.objects.filter(
-                user_profile=narrow_profile,
-                recipient__type=Recipient.HUDDLE,
-            ).values("recipient_id")
-        ]
-
-        recipient_ids = set(self_recipient_ids) & set(narrow_recipient_ids)
+        recipient_ids = self._get_huddle_recipients(narrow_profile)
         cond = column("recipient_id", Integer).in_(recipient_ids)
         return query.where(maybe_negate(cond))
 
@@ -692,8 +745,8 @@ def convert_term(elem: Union[Dict[str, Any], List[str]]) -> Dict[str, Any]:
             # that supports user IDs. Relevant code is located in web/src/message_fetch.js
             # in handle_operators_supporting_id_based_api function where you will need to update
             # operators_supporting_id, or operators_supporting_ids array.
-            operators_supporting_id = ["sender", "group-pm-with", "stream"]
-            operators_supporting_ids = ["pm-with"]
+            operators_supporting_id = ["sender", "group-pm-with", "stream", "dm-including"]
+            operators_supporting_ids = ["pm-with", "dm"]
             operators_non_empty_operand = {"search"}
 
             operator = elem.get("operator", "")
@@ -831,9 +884,9 @@ def exclude_muting_conditions(
     # muted messages exist where their absence might make conversation
     # difficult to understand. As a result, we do not need to consider
     # muted users in this server-side logic for returning messages to
-    # clients. (We could in theory exclude PMs from muted users, but
-    # they're likely to be sufficiently rare to not be worth extra
-    # logic/testing here).
+    # clients. (We could in theory exclude direct messages from muted
+    # users, but they're likely to be sufficiently rare to not be worth
+    # extra logic/testing here).
 
     return conditions
 
diff --git a/zerver/lib/url_encoding.py b/zerver/lib/url_encoding.py
--- a/zerver/lib/url_encoding.py
+++ b/zerver/lib/url_encoding.py
@@ -21,7 +21,7 @@ def encode_stream(stream_id: int, stream_name: str) -> str:
 
 
 def personal_narrow_url(realm: Realm, sender: UserProfile) -> str:
-    base_url = f"{realm.uri}/#narrow/pm-with/"
+    base_url = f"{realm.uri}/#narrow/dm/"
     encoded_user_name = re2.sub(r'[ "%\/<>`\p{C}]+', "-", sender.full_name)
     pm_slug = str(sender.id) + "-" + encoded_user_name
     return base_url + pm_slug
@@ -29,7 +29,7 @@ def personal_narrow_url(realm: Realm, sender: UserProfile) -> str:
 
 def huddle_narrow_url(realm: Realm, other_user_ids: List[int]) -> str:
     pm_slug = ",".join(str(user_id) for user_id in sorted(other_user_ids)) + "-group"
-    base_url = f"{realm.uri}/#narrow/pm-with/"
+    base_url = f"{realm.uri}/#narrow/dm/"
     return base_url + pm_slug
 
 
@@ -91,7 +91,7 @@ def near_pm_message_url(realm: Realm, message: Dict[str, Any]) -> str:
     parts = [
         realm.uri,
         "#narrow",
-        "pm-with",
+        "dm",
         pm_str,
         "near",
         message_id,


Test patch:
diff --git a/web/e2e-tests/message-basics.test.ts b/web/e2e-tests/message-basics.test.ts
--- a/web/e2e-tests/message-basics.test.ts
+++ b/web/e2e-tests/message-basics.test.ts
@@ -14,11 +14,14 @@ async function expect_home(page: Page): Promise<void> {
         ["Verona > test", ["verona test a", "verona test b"]],
         ["Verona > other topic", ["verona other topic c"]],
         ["Denmark > test", ["denmark message"]],
-        ["You and Cordelia, Lear's daughter, King Hamlet", ["group pm a", "group pm b"]],
-        ["You and Cordelia, Lear's daughter", ["pm c"]],
+        [
+            "You and Cordelia, Lear's daughter, King Hamlet",
+            ["group direct message a", "group direct message b"],
+        ],
+        ["You and Cordelia, Lear's daughter", ["direct message c"]],
         ["Verona > test", ["verona test d"]],
-        ["You and Cordelia, Lear's daughter, King Hamlet", ["group pm d"]],
-        ["You and Cordelia, Lear's daughter", ["pm e"]],
+        ["You and Cordelia, Lear's daughter, King Hamlet", ["group direct message d"]],
+        ["You and Cordelia, Lear's daughter", ["direct message e"]],
     ]);
 }
 
@@ -59,12 +62,12 @@ async function expect_test_topic(page: Page): Promise<void> {
     ]);
 }
 
-async function expect_huddle(page: Page): Promise<void> {
+async function expect_group_direct_messages(page: Page): Promise<void> {
     await page.waitForSelector("#zfilt", {visible: true});
     await common.check_messages_sent(page, "zfilt", [
         [
             "You and Cordelia, Lear's daughter, King Hamlet",
-            ["group pm a", "group pm b", "group pm d"],
+            ["group direct message a", "group direct message b", "group direct message d"],
         ],
     ]);
     assert.strictEqual(
@@ -73,10 +76,10 @@ async function expect_huddle(page: Page): Promise<void> {
     );
 }
 
-async function expect_cordelia_private_narrow(page: Page): Promise<void> {
+async function expect_cordelia_direct_messages(page: Page): Promise<void> {
     await page.waitForSelector("#zfilt", {visible: true});
     await common.check_messages_sent(page, "zfilt", [
-        ["You and Cordelia, Lear's daughter", ["pm c", "pm e"]],
+        ["You and Cordelia, Lear's daughter", ["direct message c", "direct message e"]],
     ]);
 }
 
@@ -120,7 +123,7 @@ async function test_navigations_from_home(page: Page): Promise<void> {
     await page.click(
         `#zhome [title="Narrow to your direct messages with Cordelia, Lear's daughter, King Hamlet"]`,
     );
-    await expect_huddle(page);
+    await expect_group_direct_messages(page);
 
     await un_narrow(page);
     await expect_home(page);
@@ -200,7 +203,7 @@ async function search_tests(page: Page): Promise<void> {
         page,
         "Cordelia",
         "Direct",
-        expect_cordelia_private_narrow,
+        expect_cordelia_direct_messages,
         "Cordelia, Lear's daughter - Zulip Dev - Zulip",
     );
 
@@ -242,7 +245,7 @@ async function search_tests(page: Page): Promise<void> {
 
     await search_and_check(
         page,
-        "pm-with:dummyuser@zulip.com",
+        "dm:dummyuser@zulip.com",
         "",
         expect_non_existing_user,
         "Invalid user - Zulip Dev - Zulip",
@@ -250,20 +253,23 @@ async function search_tests(page: Page): Promise<void> {
 
     await search_and_check(
         page,
-        "pm-with:dummyuser@zulip.com,dummyuser2@zulip.com",
+        "dm:dummyuser@zulip.com,dummyuser2@zulip.com",
         "",
         expect_non_existing_users,
         "Invalid users - Zulip Dev - Zulip",
     );
 }
 
-async function expect_all_pm(page: Page): Promise<void> {
+async function expect_all_direct_messages(page: Page): Promise<void> {
     await page.waitForSelector("#zfilt", {visible: true});
     await common.check_messages_sent(page, "zfilt", [
-        ["You and Cordelia, Lear's daughter, King Hamlet", ["group pm a", "group pm b"]],
-        ["You and Cordelia, Lear's daughter", ["pm c"]],
-        ["You and Cordelia, Lear's daughter, King Hamlet", ["group pm d"]],
-        ["You and Cordelia, Lear's daughter", ["pm e"]],
+        [
+            "You and Cordelia, Lear's daughter, King Hamlet",
+            ["group direct message a", "group direct message b"],
+        ],
+        ["You and Cordelia, Lear's daughter", ["direct message c"]],
+        ["You and Cordelia, Lear's daughter, King Hamlet", ["group direct message d"]],
+        ["You and Cordelia, Lear's daughter", ["direct message e"]],
     ]);
     assert.strictEqual(
         await common.get_text_from_selector(page, "#left_bar_compose_stream_button_big"),
@@ -284,7 +290,7 @@ async function test_narrow_by_clicking_the_left_sidebar(page: Page): Promise<voi
     const all_private_messages_icon = "#show_all_private_messages";
     await page.waitForSelector(all_private_messages_icon, {visible: true});
     await page.click(all_private_messages_icon);
-    await expect_all_pm(page);
+    await expect_all_direct_messages(page);
 
     await un_narrow(page);
 }
@@ -442,10 +448,10 @@ async function test_users_search(page: Page): Promise<void> {
     await assert_not_selected(page, "aaron");
     await assert_not_selected(page, "Desdemona");
 
-    // arrow up and press Enter. We should be taken to pms with Cordelia, Lear's daughter
+    // arrow up and press Enter. We should be taken to direct messages with Cordelia, Lear's daughter
     await arrow(page, "Up");
     await page.keyboard.press("Enter");
-    await expect_cordelia_private_narrow(page);
+    await expect_cordelia_direct_messages(page);
 }
 
 async function test_narrow_public_streams(page: Page): Promise<void> {
@@ -480,12 +486,12 @@ async function message_basic_tests(page: Page): Promise<void> {
         {stream: "Verona", topic: "test", content: "verona test b"},
         {stream: "Verona", topic: "other topic", content: "verona other topic c"},
         {stream: "Denmark", topic: "test", content: "denmark message"},
-        {recipient: "cordelia@zulip.com, hamlet@zulip.com", content: "group pm a"},
-        {recipient: "cordelia@zulip.com, hamlet@zulip.com", content: "group pm b"},
-        {recipient: "cordelia@zulip.com", content: "pm c"},
+        {recipient: "cordelia@zulip.com, hamlet@zulip.com", content: "group direct message a"},
+        {recipient: "cordelia@zulip.com, hamlet@zulip.com", content: "group direct message b"},
+        {recipient: "cordelia@zulip.com", content: "direct message c"},
         {stream: "Verona", topic: "test", content: "verona test d"},
-        {recipient: "cordelia@zulip.com, hamlet@zulip.com", content: "group pm d"},
-        {recipient: "cordelia@zulip.com", content: "pm e"},
+        {recipient: "cordelia@zulip.com, hamlet@zulip.com", content: "group direct message d"},
+        {recipient: "cordelia@zulip.com", content: "direct message e"},
     ]);
 
     await expect_home(page);
diff --git a/web/tests/activity.test.js b/web/tests/activity.test.js
--- a/web/tests/activity.test.js
+++ b/web/tests/activity.test.js
@@ -383,7 +383,7 @@ test("first/prev/next", ({override, mock_template}) => {
                 rendered_alice = true;
                 assert.deepEqual(data, {
                     faded: true,
-                    href: "#narrow/pm-with/1-Alice-Smith",
+                    href: "#narrow/dm/1-Alice-Smith",
                     is_current_user: false,
                     name: "Alice Smith",
                     num_unread: 0,
@@ -401,7 +401,7 @@ test("first/prev/next", ({override, mock_template}) => {
             case fred.user_id:
                 rendered_fred = true;
                 assert.deepEqual(data, {
-                    href: "#narrow/pm-with/2-Fred-Flintstone",
+                    href: "#narrow/dm/2-Fred-Flintstone",
                     name: "Fred Flintstone",
                     user_id: fred.user_id,
                     is_current_user: false,
@@ -449,7 +449,7 @@ test("insert_one_user_into_empty_list", ({override, mock_template}) => {
     user_settings.user_list_style = 2;
     mock_template("presence_row.hbs", true, (data, html) => {
         assert.deepEqual(data, {
-            href: "#narrow/pm-with/1-Alice-Smith",
+            href: "#narrow/dm/1-Alice-Smith",
             name: "Alice Smith",
             user_id: 1,
             is_current_user: false,
diff --git a/web/tests/browser_history.test.js b/web/tests/browser_history.test.js
--- a/web/tests/browser_history.test.js
+++ b/web/tests/browser_history.test.js
@@ -21,7 +21,7 @@ function test(label, f) {
 
 test("basics", () => {
     const hash1 = "#settings/profile";
-    const hash2 = "#narrow/is/private";
+    const hash2 = "#narrow/is/dm";
     browser_history.go_to_location(hash1);
     assert.equal(window.location.hash, hash1);
 
@@ -69,7 +69,7 @@ test("update internal hash if required", ({override_rewire}) => {
 test("web-public view hash restore", () => {
     browser_history.update("#");
     assert.equal(window.location.hash, "");
-    const new_hash = "#narrow/is/private";
+    const new_hash = "#narrow/is/dm";
     browser_history.update(new_hash);
     assert.equal(window.location.hash, new_hash);
     browser_history.return_to_web_public_hash();
diff --git a/web/tests/buddy_data.test.js b/web/tests/buddy_data.test.js
--- a/web/tests/buddy_data.test.js
+++ b/web/tests/buddy_data.test.js
@@ -510,7 +510,7 @@ test("get_items_for_users", () => {
     assert.deepEqual(buddy_data.get_items_for_users(user_ids), [
         {
             faded: false,
-            href: "#narrow/pm-with/1001-Human-Myself",
+            href: "#narrow/dm/1001-Human-Myself",
             is_current_user: true,
             name: "Human Myself",
             num_unread: 0,
@@ -522,7 +522,7 @@ test("get_items_for_users", () => {
         },
         {
             faded: false,
-            href: "#narrow/pm-with/1002-Alice-Smith",
+            href: "#narrow/dm/1002-Alice-Smith",
             is_current_user: false,
             name: "Alice Smith",
             num_unread: 0,
@@ -534,7 +534,7 @@ test("get_items_for_users", () => {
         },
         {
             faded: false,
-            href: "#narrow/pm-with/1003-Fred-Flintstone",
+            href: "#narrow/dm/1003-Fred-Flintstone",
             is_current_user: false,
             name: "Fred Flintstone",
             num_unread: 0,
diff --git a/web/tests/example8.test.js b/web/tests/example8.test.js
--- a/web/tests/example8.test.js
+++ b/web/tests/example8.test.js
@@ -77,7 +77,7 @@ run_test("typing_events.render_notifications_for_narrow", ({override, mock_templ
     override(page_params, "user_id", anna.user_id);
     const group = [anna.user_id, vronsky.user_id, levin.user_id, kitty.user_id];
     const group_emails = `${anna.email},${vronsky.email},${levin.email},${kitty.email}`;
-    narrow_state.set_current_filter(new Filter([{operator: "pm-with", operand: group_emails}]));
+    narrow_state.set_current_filter(new Filter([{operator: "dm", operand: group_emails}]));
 
     // Based on typing_events.MAX_USERS_TO_DISPLAY_NAME (which is currently 3),
     // we display either the list of all users typing (if they do not exceed
diff --git a/web/tests/filter.test.js b/web/tests/filter.test.js
--- a/web/tests/filter.test.js
+++ b/web/tests/filter.test.js
@@ -194,7 +194,7 @@ test("basics", () => {
     assert.ok(!filter.is_personal_filter());
     assert.ok(!filter.is_conversation_view());
 
-    operators = [{operator: "is", operand: "private"}];
+    operators = [{operator: "is", operand: "dm"}];
     filter = new Filter(operators);
     assert.ok(filter.contains_only_private_messages());
     assert.ok(filter.can_mark_messages_read());
@@ -204,6 +204,12 @@ test("basics", () => {
     assert.ok(!filter.is_personal_filter());
     assert.ok(!filter.is_conversation_view());
 
+    // "is:private" was renamed to "is:dm"
+    operators = [{operator: "is", operand: "private"}];
+    filter = new Filter(operators);
+    assert.ok(filter.has_operand("is", "dm"));
+    assert.ok(!filter.has_operand("is", "private"));
+
     operators = [{operator: "is", operand: "mentioned"}];
     filter = new Filter(operators);
     assert.ok(!filter.contains_only_private_messages());
@@ -224,7 +230,7 @@ test("basics", () => {
     assert.ok(filter.is_personal_filter());
     assert.ok(!filter.is_conversation_view());
 
-    operators = [{operator: "pm-with", operand: "joe@example.com"}];
+    operators = [{operator: "dm", operand: "joe@example.com"}];
     filter = new Filter(operators);
     assert.ok(filter.is_non_huddle_pm());
     assert.ok(filter.contains_only_private_messages());
@@ -235,7 +241,7 @@ test("basics", () => {
     assert.ok(!filter.is_personal_filter());
     assert.ok(filter.is_conversation_view());
 
-    operators = [{operator: "pm-with", operand: "joe@example.com,jack@example.com"}];
+    operators = [{operator: "dm", operand: "joe@example.com,jack@example.com"}];
     filter = new Filter(operators);
     assert.ok(!filter.is_non_huddle_pm());
     assert.ok(filter.contains_only_private_messages());
@@ -245,7 +251,13 @@ test("basics", () => {
     assert.ok(!filter.is_personal_filter());
     assert.ok(filter.is_conversation_view());
 
-    operators = [{operator: "group-pm-with", operand: "joe@example.com"}];
+    // "pm-with" was renamed to "dm"
+    operators = [{operator: "pm-with", operand: "joe@example.com"}];
+    filter = new Filter(operators);
+    assert.ok(filter.has_operator("dm"));
+    assert.ok(!filter.has_operator("    pm-with"));
+
+    operators = [{operator: "dm-including", operand: "joe@example.com"}];
     filter = new Filter(operators);
     assert.ok(!filter.is_non_huddle_pm());
     assert.ok(filter.contains_only_private_messages());
@@ -256,6 +268,12 @@ test("basics", () => {
     assert.ok(!filter.is_personal_filter());
     assert.ok(!filter.is_conversation_view());
 
+    // "group-pm-with" was replaced with "dm-including"
+    operators = [{operator: "group-pm-with", operand: "joe@example.com"}];
+    filter = new Filter(operators);
+    assert.ok(filter.has_operator("dm-including"));
+    assert.ok(!filter.has_operator("group-pm-with"));
+
     operators = [{operator: "is", operand: "resolved"}];
     filter = new Filter(operators);
     assert.ok(!filter.contains_only_private_messages());
@@ -270,7 +288,7 @@ test("basics", () => {
     // filter.supports_collapsing_recipients loop.
     operators = [
         {operator: "is", operand: "resolved", negated: true},
-        {operator: "is", operand: "private", negated: true},
+        {operator: "is", operand: "dm", negated: true},
         {operator: "stream", operand: "stream_name", negated: true},
         {operator: "streams", operand: "web-public", negated: true},
         {operator: "streams", operand: "public"},
@@ -426,30 +444,30 @@ test("can_mark_messages_read", () => {
     filter = new Filter(stream_negated_topic_operators);
     assert.ok(!filter.can_mark_messages_read());
 
-    const pm_with = [{operator: "pm-with", operand: "joe@example.com,"}];
+    const dm = [{operator: "dm", operand: "joe@example.com,"}];
 
-    const pm_with_negated = [{operator: "pm-with", operand: "joe@example.com,", negated: true}];
+    const dm_negated = [{operator: "dm", operand: "joe@example.com,", negated: true}];
 
-    const group_pm = [{operator: "pm-with", operand: "joe@example.com,STEVE@foo.com"}];
-    filter = new Filter(pm_with);
+    const dm_group = [{operator: "dm", operand: "joe@example.com,STEVE@foo.com"}];
+    filter = new Filter(dm);
     assert.ok(filter.can_mark_messages_read());
-    filter = new Filter(pm_with_negated);
+    filter = new Filter(dm_negated);
     assert.ok(!filter.can_mark_messages_read());
-    filter = new Filter(group_pm);
+    filter = new Filter(dm_group);
     assert.ok(filter.can_mark_messages_read());
-    assert_not_mark_read_with_is_operands(group_pm);
-    assert_not_mark_read_with_is_operands(pm_with);
-    assert_not_mark_read_with_has_operands(group_pm);
-    assert_not_mark_read_with_has_operands(pm_with);
-    assert_not_mark_read_when_searching(group_pm);
-    assert_not_mark_read_when_searching(pm_with);
-
-    const is_private = [{operator: "is", operand: "private"}];
-    filter = new Filter(is_private);
+    assert_not_mark_read_with_is_operands(dm_group);
+    assert_not_mark_read_with_is_operands(dm);
+    assert_not_mark_read_with_has_operands(dm_group);
+    assert_not_mark_read_with_has_operands(dm);
+    assert_not_mark_read_when_searching(dm_group);
+    assert_not_mark_read_when_searching(dm);
+
+    const is_dm = [{operator: "is", operand: "dm"}];
+    filter = new Filter(is_dm);
     assert.ok(filter.can_mark_messages_read());
-    assert_not_mark_read_with_is_operands(is_private);
-    assert_not_mark_read_with_has_operands(is_private);
-    assert_not_mark_read_when_searching(is_private);
+    assert_not_mark_read_with_is_operands(is_dm);
+    assert_not_mark_read_with_has_operands(is_dm);
+    assert_not_mark_read_when_searching(is_dm);
 
     const in_all = [{operator: "in", operand: "all"}];
     filter = new Filter(in_all);
@@ -478,7 +496,7 @@ test("can_mark_messages_read", () => {
 
     // test caching of term types
     // init and stub
-    filter = new Filter(pm_with);
+    filter = new Filter(dm);
     filter.stub = filter.calc_can_mark_messages_read;
     filter.calc_can_mark_messages_read = function () {
         this.calc_can_mark_messages_read_called = true;
@@ -592,18 +610,18 @@ test("redundancies", () => {
     let filter;
 
     terms = [
-        {operator: "pm-with", operand: "joe@example.com,"},
-        {operator: "is", operand: "private"},
+        {operator: "dm", operand: "joe@example.com,"},
+        {operator: "is", operand: "dm"},
     ];
     filter = new Filter(terms);
-    assert.ok(filter.can_bucket_by("pm-with"));
+    assert.ok(filter.can_bucket_by("dm"));
 
     terms = [
-        {operator: "pm-with", operand: "joe@example.com,", negated: true},
-        {operator: "is", operand: "private"},
+        {operator: "dm", operand: "joe@example.com,", negated: true},
+        {operator: "is", operand: "dm"},
     ];
     filter = new Filter(terms);
-    assert.ok(filter.can_bucket_by("is-private", "not-pm-with"));
+    assert.ok(filter.can_bucket_by("is-dm", "not-dm"));
 });
 
 test("canonicalization", () => {
@@ -621,10 +639,16 @@ test("canonicalization", () => {
     assert.equal(term.operator, "sender");
     assert.equal(term.operand, "me@example.com");
 
+    // "pm-with" was renamed to "dm"
     term = Filter.canonicalize_term({operator: "pm-with", operand: "me"});
-    assert.equal(term.operator, "pm-with");
+    assert.equal(term.operator, "dm");
     assert.equal(term.operand, "me@example.com");
 
+    // "group-pm-with" was replaced with "dm-including"
+    term = Filter.canonicalize_term({operator: "group-pm-with", operand: "joe@example.com"});
+    assert.equal(term.operator, "dm-including");
+    assert.equal(term.operand, "joe@example.com");
+
     term = Filter.canonicalize_term({operator: "search", operand: "foo"});
     assert.equal(term.operator, "search");
     assert.equal(term.operand, "foo");
@@ -690,7 +714,7 @@ test("predicate_basics", () => {
     predicate = get_predicate([["topic", "Bar"]]);
     assert.ok(!predicate({type: "private"}));
 
-    predicate = get_predicate([["is", "private"]]);
+    predicate = get_predicate([["is", "dm"]]);
     assert.ok(predicate({type: "private"}));
     assert.ok(!predicate({type: "stream"}));
 
@@ -751,7 +775,7 @@ test("predicate_basics", () => {
     assert.ok(predicate({sender_id: joe.user_id}));
     assert.ok(!predicate({sender_email: steve.user_id}));
 
-    predicate = get_predicate([["pm-with", "Joe@example.com"]]);
+    predicate = get_predicate([["dm", "Joe@example.com"]]);
     assert.ok(
         predicate({
             type: "private",
@@ -772,7 +796,7 @@ test("predicate_basics", () => {
     );
     assert.ok(!predicate({type: "stream"}));
 
-    predicate = get_predicate([["pm-with", "Joe@example.com,steve@foo.com"]]);
+    predicate = get_predicate([["dm", "Joe@example.com,steve@foo.com"]]);
     assert.ok(
         predicate({
             type: "private",
@@ -781,7 +805,7 @@ test("predicate_basics", () => {
     );
 
     // Make sure your own email is ignored
-    predicate = get_predicate([["pm-with", "Joe@example.com,steve@foo.com,me@example.com"]]);
+    predicate = get_predicate([["dm", "Joe@example.com,steve@foo.com,me@example.com"]]);
     assert.ok(
         predicate({
             type: "private",
@@ -789,7 +813,7 @@ test("predicate_basics", () => {
         }),
     );
 
-    predicate = get_predicate([["pm-with", "nobody@example.com"]]);
+    predicate = get_predicate([["dm", "nobody@example.com"]]);
     assert.ok(
         !predicate({
             type: "private",
@@ -797,15 +821,15 @@ test("predicate_basics", () => {
         }),
     );
 
-    predicate = get_predicate([["group-pm-with", "nobody@example.com"]]);
+    predicate = get_predicate([["dm-including", "nobody@example.com"]]);
     assert.ok(
         !predicate({
             type: "private",
-            display_recipient: [{id: joe.user_id}],
+            display_recipient: [{id: joe.user_id}, {id: me.user_id}],
         }),
     );
 
-    predicate = get_predicate([["group-pm-with", "Joe@example.com"]]);
+    predicate = get_predicate([["dm-including", "Joe@example.com"]]);
     assert.ok(
         predicate({
             type: "private",
@@ -813,10 +837,9 @@ test("predicate_basics", () => {
         }),
     );
     assert.ok(
-        !predicate({
-            // you must be a part of the group pm
+        predicate({
             type: "private",
-            display_recipient: [{id: joe.user_id}, {id: steve.user_id}],
+            display_recipient: [{id: joe.user_id}, {id: me.user_id}],
         }),
     );
     assert.ok(
@@ -994,8 +1017,8 @@ test("parse", () => {
     ];
     _test();
 
-    string = "pm-with:leo+test@zulip.com";
-    operators = [{operator: "pm-with", operand: "leo+test@zulip.com"}];
+    string = "dm:leo+test@zulip.com";
+    operators = [{operator: "dm", operand: "leo+test@zulip.com"}];
     _test();
 
     string = "sender:leo+test@zulip.com";
@@ -1167,7 +1190,7 @@ test("describe", () => {
     assert.equal(Filter.describe(narrow), string);
 
     narrow = [
-        {operator: "is", operand: "private"},
+        {operator: "is", operand: "dm"},
         {operator: "search", operand: "lunch"},
     ];
     string = "direct messages, search for lunch";
@@ -1210,7 +1233,7 @@ test("describe", () => {
     assert.equal(Filter.describe(narrow), string);
 
     narrow = [
-        {operator: "is", operand: "private"},
+        {operator: "is", operand: "dm"},
         {operator: "search", operand: "lunch", negated: true},
     ];
     string = "direct messages, exclude lunch";
@@ -1254,7 +1277,7 @@ test("can_bucket_by", () => {
     let filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("stream"), true);
     assert.equal(filter.can_bucket_by("stream", "topic"), false);
-    assert.equal(filter.can_bucket_by("pm-with"), false);
+    assert.equal(filter.can_bucket_by("dm"), false);
 
     terms = [
         // try a non-orthodox ordering
@@ -1264,7 +1287,7 @@ test("can_bucket_by", () => {
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("stream"), true);
     assert.equal(filter.can_bucket_by("stream", "topic"), true);
-    assert.equal(filter.can_bucket_by("pm-with"), false);
+    assert.equal(filter.can_bucket_by("dm"), false);
 
     terms = [
         {operator: "stream", operand: "My stream", negated: true},
@@ -1273,31 +1296,31 @@ test("can_bucket_by", () => {
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("stream"), false);
     assert.equal(filter.can_bucket_by("stream", "topic"), false);
-    assert.equal(filter.can_bucket_by("pm-with"), false);
+    assert.equal(filter.can_bucket_by("dm"), false);
 
-    terms = [{operator: "pm-with", operand: "foo@example.com", negated: true}];
+    terms = [{operator: "dm", operand: "foo@example.com", negated: true}];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("stream"), false);
     assert.equal(filter.can_bucket_by("stream", "topic"), false);
-    assert.equal(filter.can_bucket_by("pm-with"), false);
+    assert.equal(filter.can_bucket_by("dm"), false);
 
-    terms = [{operator: "pm-with", operand: "foo@example.com,bar@example.com"}];
+    terms = [{operator: "dm", operand: "foo@example.com,bar@example.com"}];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("stream"), false);
     assert.equal(filter.can_bucket_by("stream", "topic"), false);
-    assert.equal(filter.can_bucket_by("pm-with"), true);
+    assert.equal(filter.can_bucket_by("dm"), true);
     assert.equal(filter.can_bucket_by("is-mentioned"), false);
-    assert.equal(filter.can_bucket_by("is-private"), false);
+    assert.equal(filter.can_bucket_by("is-dm"), false);
 
-    terms = [{operator: "is", operand: "private"}];
+    terms = [{operator: "is", operand: "dm"}];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("is-mentioned"), false);
-    assert.equal(filter.can_bucket_by("is-private"), true);
+    assert.equal(filter.can_bucket_by("is-dm"), true);
 
     terms = [{operator: "is", operand: "mentioned"}];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("is-mentioned"), true);
-    assert.equal(filter.can_bucket_by("is-private"), false);
+    assert.equal(filter.can_bucket_by("is-dm"), false);
 
     terms = [
         {operator: "is", operand: "mentioned"},
@@ -1305,25 +1328,25 @@ test("can_bucket_by", () => {
     ];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("is-mentioned"), true);
-    assert.equal(filter.can_bucket_by("is-private"), false);
+    assert.equal(filter.can_bucket_by("is-dm"), false);
 
     // The call below returns false for somewhat arbitrary
-    // reasons -- we say is-private has precedence over
+    // reasons -- we say is-dm has precedence over
     // is-starred.
     assert.equal(filter.can_bucket_by("is-starred"), false);
 
     terms = [{operator: "is", operand: "mentioned", negated: true}];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("is-mentioned"), false);
-    assert.equal(filter.can_bucket_by("is-private"), false);
+    assert.equal(filter.can_bucket_by("is-dm"), false);
 
     terms = [{operator: "is", operand: "resolved"}];
     filter = new Filter(terms);
     assert.equal(filter.can_bucket_by("stream", "topic"), false);
     assert.equal(filter.can_bucket_by("stream"), false);
-    assert.equal(filter.can_bucket_by("pm-with"), false);
+    assert.equal(filter.can_bucket_by("dm"), false);
     assert.equal(filter.can_bucket_by("is-mentioned"), false);
-    assert.equal(filter.can_bucket_by("is-private"), false);
+    assert.equal(filter.can_bucket_by("is-dm"), false);
 });
 
 test("term_type", () => {
@@ -1341,8 +1364,9 @@ test("term_type", () => {
 
     assert_term_type(term("streams", "public"), "streams-public");
     assert_term_type(term("stream", "whatever"), "stream");
-    assert_term_type(term("pm-with", "whomever"), "pm-with");
-    assert_term_type(term("pm-with", "whomever", true), "not-pm-with");
+    assert_term_type(term("dm", "whomever"), "dm");
+    assert_term_type(term("dm", "whomever", true), "not-dm");
+    assert_term_type(term("is", "dm"), "is-dm");
     assert_term_type(term("is", "private"), "is-private");
     assert_term_type(term("has", "link"), "has-link");
     assert_term_type(term("has", "attachment", true), "not-has-attachment");
@@ -1355,8 +1379,8 @@ test("term_type", () => {
     assert_term_sort(["topic", "stream", "sender"], ["stream", "topic", "sender"]);
 
     assert_term_sort(
-        ["has-link", "near", "is-unread", "pm-with"],
-        ["pm-with", "near", "is-unread", "has-link"],
+        ["has-link", "near", "is-unread", "dm"],
+        ["dm", "near", "is-unread", "has-link"],
     );
 
     assert_term_sort(["bogus", "stream", "topic"], ["stream", "topic", "bogus"]);
@@ -1420,13 +1444,13 @@ test("first_valid_id_from", ({override}) => {
 
 test("update_email", () => {
     const terms = [
-        {operator: "pm-with", operand: "steve@foo.com"},
+        {operator: "dm", operand: "steve@foo.com"},
         {operator: "sender", operand: "steve@foo.com"},
         {operator: "stream", operand: "steve@foo.com"}, // try to be tricky
     ];
     const filter = new Filter(terms);
     filter.update_email(steve.user_id, "showell@foo.com");
-    assert.deepEqual(filter.operands("pm-with"), ["showell@foo.com"]);
+    assert.deepEqual(filter.operands("dm"), ["showell@foo.com"]);
     assert.deepEqual(filter.operands("sender"), ["showell@foo.com"]);
     assert.deepEqual(filter.operands("stream"), ["steve@foo.com"]);
 });
@@ -1493,7 +1517,7 @@ test("navbar_helpers", () => {
     const in_home = [{operator: "in", operand: "home"}];
     const in_all = [{operator: "in", operand: "all"}];
     const is_starred = [{operator: "is", operand: "starred"}];
-    const is_private = [{operator: "is", operand: "private"}];
+    const is_dm = [{operator: "is", operand: "dm"}];
     const is_mentioned = [{operator: "is", operand: "mentioned"}];
     const is_resolved = [{operator: "is", operand: "resolved"}];
     const streams_public = [{operator: "streams", operand: "public"}];
@@ -1512,10 +1536,10 @@ test("navbar_helpers", () => {
         {operator: "stream", operand: "Elephant"},
         {operator: "topic", operand: "pink"},
     ];
-    const pm_with = [{operator: "pm-with", operand: "joe@example.com"}];
-    const group_pm = [{operator: "pm-with", operand: "joe@example.com,STEVE@foo.com"}];
-    const group_pm_including_missing_person = [
-        {operator: "pm-with", operand: "joe@example.com,STEVE@foo.com,sally@doesnotexist.com"},
+    const dm = [{operator: "dm", operand: "joe@example.com"}];
+    const dm_group = [{operator: "dm", operand: "joe@example.com,STEVE@foo.com"}];
+    const dm_group_including_missing_person = [
+        {operator: "dm", operand: "joe@example.com,STEVE@foo.com,sally@doesnotexist.com"},
     ];
     // not common narrows, but used for browser title updates
     const is_alerted = [{operator: "is", operand: "alerted"}];
@@ -1525,8 +1549,8 @@ test("navbar_helpers", () => {
         {operator: "topic", operand: "bar"},
         {operator: "near", operand: "12"},
     ];
-    const pm_with_near = [
-        {operator: "pm-with", operand: "joe@example.com"},
+    const dm_near = [
+        {operator: "dm", operand: "joe@example.com"},
         {operator: "near", operand: "12"},
     ];
 
@@ -1553,11 +1577,11 @@ test("navbar_helpers", () => {
             redirect_url_with_search: "#",
         },
         {
-            operator: is_private,
+            operator: is_dm,
             is_common_narrow: true,
             icon: "envelope",
             title: "translated: Direct messages",
-            redirect_url_with_search: "/#narrow/is/private",
+            redirect_url_with_search: "/#narrow/is/dm",
         },
         {
             operator: is_mentioned,
@@ -1623,23 +1647,22 @@ test("navbar_helpers", () => {
             redirect_url_with_search: "/#narrow/stream/12-webPublicSub",
         },
         {
-            operator: pm_with,
+            operator: dm,
             is_common_narrow: true,
             icon: "envelope",
             title: properly_separated_names([joe.full_name]),
             redirect_url_with_search:
-                "/#narrow/pm-with/" + joe.user_id + "-" + parseOneAddress(joe.email).local,
+                "/#narrow/dm/" + joe.user_id + "-" + parseOneAddress(joe.email).local,
         },
         {
-            operator: group_pm,
+            operator: dm_group,
             is_common_narrow: true,
             icon: "envelope",
             title: properly_separated_names([joe.full_name, steve.full_name]),
-            redirect_url_with_search:
-                "/#narrow/pm-with/" + joe.user_id + "," + steve.user_id + "-group",
+            redirect_url_with_search: "/#narrow/dm/" + joe.user_id + "," + steve.user_id + "-group",
         },
         {
-            operator: group_pm_including_missing_person,
+            operator: dm_group_including_missing_person,
             is_common_narrow: true,
             icon: "envelope",
             title: properly_separated_names([
@@ -1647,7 +1670,7 @@ test("navbar_helpers", () => {
                 steve.full_name,
                 "sally@doesnotexist.com",
             ]),
-            redirect_url_with_search: "/#narrow/pm-with/undefined",
+            redirect_url_with_search: "/#narrow/dm/undefined",
         },
         {
             operator: is_alerted,
@@ -1671,7 +1694,7 @@ test("navbar_helpers", () => {
             redirect_url_with_search: "#",
         },
         {
-            operator: pm_with_near,
+            operator: dm_near,
             is_common_narrow: false,
             icon: "envelope",
             title: properly_separated_names([joe.full_name]),
@@ -1750,7 +1773,7 @@ test("error_cases", () => {
     // This test just gives us 100% line coverage on defensive code that
     // should not be reached unless we break other code.
 
-    const predicate = get_predicate([["pm-with", "Joe@example.com"]]);
+    const predicate = get_predicate([["dm", "Joe@example.com"]]);
     blueslip.expect("error", "Empty recipient list in message");
     assert.ok(!predicate({type: "private", display_recipient: []}));
 });
@@ -1769,11 +1792,9 @@ run_test("is_spectator_compatible", () => {
         ]),
     );
     assert.ok(Filter.is_spectator_compatible([{operator: "sender", operand: "hamlet@zulip.com"}]));
+    assert.ok(!Filter.is_spectator_compatible([{operator: "dm", operand: "hamlet@zulip.com"}]));
     assert.ok(
-        !Filter.is_spectator_compatible([{operator: "pm-with", operand: "hamlet@zulip.com"}]),
-    );
-    assert.ok(
-        !Filter.is_spectator_compatible([{operator: "group-pm-with", operand: "hamlet@zulip.com"}]),
+        !Filter.is_spectator_compatible([{operator: "dm-including", operand: "hamlet@zulip.com"}]),
     );
     assert.ok(Filter.is_spectator_compatible([{operator: "stream", operand: "Denmark"}]));
     assert.ok(
@@ -1783,8 +1804,20 @@ run_test("is_spectator_compatible", () => {
         ]),
     );
     assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "starred"}]));
-    assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "private"}]));
+    assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "dm"}]));
     assert.ok(Filter.is_spectator_compatible([{operator: "streams", operand: "public"}]));
+
     // Malformed input not allowed
     assert.ok(!Filter.is_spectator_compatible([{operator: "has"}]));
+
+    // "is:private" was renamed to "is:dm"
+    assert.ok(!Filter.is_spectator_compatible([{operator: "is", operand: "private"}]));
+    // "pm-with" was renamed to "dm"
+    assert.ok(
+        !Filter.is_spectator_compatible([{operator: "pm-with", operand: "hamlet@zulip.com"}]),
+    );
+    // "group-pm-with:" was replaced with "dm-including:"
+    assert.ok(
+        !Filter.is_spectator_compatible([{operator: "group-pm-with", operand: "hamlet@zulip.com"}]),
+    );
 });
diff --git a/web/tests/hash_util.test.js b/web/tests/hash_util.test.js
--- a/web/tests/hash_util.test.js
+++ b/web/tests/hash_util.test.js
@@ -162,7 +162,7 @@ run_test("test_by_conversation_and_time_url", () => {
 
     assert.equal(
         hash_util.by_conversation_and_time_url(message),
-        "http://zulip.zulipdev.com/#narrow/pm-with/15-pm/near/43",
+        "http://zulip.zulipdev.com/#narrow/dm/15-dm/near/43",
     );
 });
 
diff --git a/web/tests/hashchange.test.js b/web/tests/hashchange.test.js
--- a/web/tests/hashchange.test.js
+++ b/web/tests/hashchange.test.js
@@ -95,6 +95,7 @@ run_test("operators_trailing_slash", () => {
 run_test("people_slugs", () => {
     let operators;
     let hash;
+    let narrow;
 
     const alice = {
         email: "alice@example.com",
@@ -106,12 +107,22 @@ run_test("people_slugs", () => {
     operators = [{operator: "sender", operand: "alice@example.com"}];
     hash = hash_util.operators_to_hash(operators);
     assert.equal(hash, "#narrow/sender/42-Alice-Smith");
-    const narrow = hash_util.parse_narrow(hash.split("/"));
+    narrow = hash_util.parse_narrow(hash.split("/"));
     assert.deepEqual(narrow, [{operator: "sender", operand: "alice@example.com", negated: false}]);
 
+    operators = [{operator: "dm", operand: "alice@example.com"}];
+    hash = hash_util.operators_to_hash(operators);
+    assert.equal(hash, "#narrow/dm/42-Alice-Smith");
+    narrow = hash_util.parse_narrow(hash.split("/"));
+    assert.deepEqual(narrow, [{operator: "dm", operand: "alice@example.com", negated: false}]);
+
+    // Even though we renamed "pm-with" to "dm", preexisting
+    // links/URLs with "pm-with" operator are handled correctly.
     operators = [{operator: "pm-with", operand: "alice@example.com"}];
     hash = hash_util.operators_to_hash(operators);
     assert.equal(hash, "#narrow/pm-with/42-Alice-Smith");
+    narrow = hash_util.parse_narrow(hash.split("/"));
+    assert.deepEqual(narrow, [{operator: "pm-with", operand: "alice@example.com", negated: false}]);
 });
 
 function test_helper({override, change_tab}) {
@@ -321,13 +332,13 @@ run_test("hash_interactions", ({override}) => {
 run_test("save_narrow", ({override}) => {
     const helper = test_helper({override});
 
-    let operators = [{operator: "is", operand: "private"}];
+    let operators = [{operator: "is", operand: "dm"}];
 
     blueslip.expect("warn", "browser does not support pushState");
     hashchange.save_narrow(operators);
 
     helper.assert_events([[message_viewport, "stop_auto_scrolling"]]);
-    assert.equal(window.location.hash, "#narrow/is/private");
+    assert.equal(window.location.hash, "#narrow/is/dm");
 
     let url_pushed;
     override(history, "pushState", (state, title, url) => {
diff --git a/web/tests/message_fetch.test.js b/web/tests/message_fetch.test.js
--- a/web/tests/message_fetch.test.js
+++ b/web/tests/message_fetch.test.js
@@ -293,7 +293,7 @@ run_test("initialize", () => {
 });
 
 function simulate_narrow() {
-    const filter = new Filter([{operator: "pm-with", operand: alice.email}]);
+    const filter = new Filter([{operator: "dm", operand: alice.email}]);
 
     const msg_list = new message_list.MessageList({
         table_name: "zfilt",
@@ -364,7 +364,7 @@ run_test("loading_newer", () => {
                 anchor: "444",
                 num_before: 0,
                 num_after: 100,
-                narrow: `[{"negated":false,"operator":"pm-with","operand":[${alice.user_id}]}]`,
+                narrow: `[{"negated":false,"operator":"dm","operand":[${alice.user_id}]}]`,
                 client_gravatar: true,
             },
             resp: {
diff --git a/web/tests/message_list_data.test.js b/web/tests/message_list_data.test.js
--- a/web/tests/message_list_data.test.js
+++ b/web/tests/message_list_data.test.js
@@ -126,7 +126,7 @@ run_test("basics", () => {
 run_test("muting", () => {
     let mld = new MessageListData({
         excludes_muted_topics: false,
-        filter: new Filter([{operator: "pm-with", operand: "alice@example.com"}]),
+        filter: new Filter([{operator: "dm", operand: "alice@example.com"}]),
     });
 
     const msgs = [
diff --git a/web/tests/narrow.test.js b/web/tests/narrow.test.js
--- a/web/tests/narrow.test.js
+++ b/web/tests/narrow.test.js
@@ -175,15 +175,26 @@ run_test("urls", () => {
     people.initialize_current_user(me.user_id);
 
     let url = hash_util.pm_with_url(ray.email);
-    assert.equal(url, "#narrow/pm-with/22-Raymond");
+    assert.equal(url, "#narrow/dm/22-Raymond");
 
     url = hash_util.huddle_with_url("22,23");
-    assert.equal(url, "#narrow/pm-with/22,23-group");
+    assert.equal(url, "#narrow/dm/22,23-group");
 
     url = hash_util.by_sender_url(ray.email);
     assert.equal(url, "#narrow/sender/22-Raymond");
 
-    let emails = hash_util.decode_operand("pm-with", "22,23-group");
+    let emails = hash_util.decode_operand("dm", "22,23-group");
+    assert.equal(emails, "alice@example.com,ray@example.com");
+
+    emails = hash_util.decode_operand("dm", "5,22,23-group");
+    assert.equal(emails, "alice@example.com,ray@example.com");
+
+    emails = hash_util.decode_operand("dm", "5-group");
+    assert.equal(emails, "me@example.com");
+
+    // Even though we renamed "pm-with" to "dm", preexisting
+    // links/URLs with "pm-with" operator are decoded correctly.
+    emails = hash_util.decode_operand("pm-with", "22,23-group");
     assert.equal(emails, "alice@example.com,ray@example.com");
 
     emails = hash_util.decode_operand("pm-with", "5,22,23-group");
@@ -286,10 +297,10 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    // organization has disabled sending private messages
+    // organization has disabled sending direct messages
     page_params.realm_private_message_policy =
         settings_config.private_message_policy_values.disabled.code;
-    set_filter([["is", "private"]]);
+    set_filter([["is", "dm"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -298,10 +309,10 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    // sending private messages enabled
+    // sending direct messages enabled
     page_params.realm_private_message_policy =
         settings_config.private_message_policy_values.by_anyone.code;
-    set_filter([["is", "private"]]);
+    set_filter([["is", "dm"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -325,12 +336,12 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         empty_narrow_html("translated: No topics are marked as resolved."),
     );
 
-    // organization has disabled sending private messages
+    // organization has disabled sending direct messages
     page_params.realm_private_message_policy =
         settings_config.private_message_policy_values.disabled.code;
 
     // prioritize information about invalid user(s) in narrow/search
-    set_filter([["pm-with", ["Yo"]]]);
+    set_filter([["dm", ["Yo"]]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -338,14 +349,14 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
     );
 
     people.add_active_user(alice);
-    set_filter([["pm-with", ["alice@example.com", "Yo"]]]);
+    set_filter([["dm", ["alice@example.com", "Yo"]]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
         empty_narrow_html("translated: One or more of these users do not exist!"),
     );
 
-    set_filter([["pm-with", "alice@example.com"]]);
+    set_filter([["dm", "alice@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -354,10 +365,10 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    // private messages with a bot are possible even though
-    // the organization has disabled sending private messages
+    // direct messages with a bot are possible even though
+    // the organization has disabled sending direct messages
     people.add_active_user(bot);
-    set_filter([["pm-with", "bot@example.com"]]);
+    set_filter([["dm", "bot@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -367,9 +378,9 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    // group private messages with bots are not possible when
-    // sending private messages is disabled
-    set_filter([["pm-with", bot.email + "," + alice.email]]);
+    // group direct messages with bots are not possible when
+    // sending direct messages is disabled
+    set_filter([["dm", bot.email + "," + alice.email]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -378,10 +389,10 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    // sending private messages enabled
+    // sending direct messages enabled
     page_params.realm_private_message_policy =
         settings_config.private_message_policy_values.by_anyone.code;
-    set_filter([["pm-with", "alice@example.com"]]);
+    set_filter([["dm", "alice@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -393,7 +404,7 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
 
     people.add_active_user(me);
     people.initialize_current_user(me.user_id);
-    set_filter([["pm-with", me.email]]);
+    set_filter([["dm", me.email]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -403,7 +414,7 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    set_filter([["pm-with", me.email + "," + alice.email]]);
+    set_filter([["dm", me.email + "," + alice.email]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
@@ -413,51 +424,59 @@ run_test("show_empty_narrow_message", ({mock_template}) => {
         ),
     );
 
-    // organization has disabled sending private messages
+    // organization has disabled sending direct messages
     page_params.realm_private_message_policy =
         settings_config.private_message_policy_values.disabled.code;
 
     // prioritize information about invalid user in narrow/search
-    set_filter([["group-pm-with", ["Yo"]]]);
+    set_filter([["dm-including", ["Yo"]]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
         empty_narrow_html("translated: This user does not exist!"),
     );
 
-    set_filter([["group-pm-with", "alice@example.com"]]);
+    set_filter([["dm-including", "alice@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
         empty_narrow_html(
-            "translated: You are not allowed to send group direct messages in this organization.",
+            "translated: You are not allowed to send direct messages in this organization.",
         ),
     );
 
-    // group private messages with bots are not possible when
-    // sending private messages is disabled
-    set_filter([["group-pm-with", "bot@example.com"]]);
+    // direct messages with a bot are possible even though
+    // the organization has disabled sending direct messages
+    set_filter([["dm-including", "bot@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
         empty_narrow_html(
-            "translated: You are not allowed to send group direct messages in this organization.",
+            "translated: You have no direct messages including Example Bot yet.",
+            'translated HTML: Why not <a href="#" class="empty_feed_compose_private">start the conversation</a>?',
         ),
     );
 
-    // sending private messages enabled
+    // sending direct messages enabled
     page_params.realm_private_message_policy =
         settings_config.private_message_policy_values.by_anyone.code;
-    set_filter([["group-pm-with", "alice@example.com"]]);
+    set_filter([["dm-including", "alice@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
         $(".empty_feed_notice_main").html(),
         empty_narrow_html(
-            "translated: You have no group direct messages with Alice Smith yet.",
+            "translated: You have no direct messages including Alice Smith yet.",
             'translated HTML: Why not <a href="#" class="empty_feed_compose_private">start the conversation</a>?',
         ),
     );
 
+    set_filter([["dm-including", me.email]]);
+    narrow_banner.show_empty_narrow_message();
+    assert.equal(
+        $(".empty_feed_notice_main").html(),
+        empty_narrow_html("translated: You don't have any direct message conversations yet."),
+    );
+
     set_filter([["sender", "ray@example.com"]]);
     narrow_banner.show_empty_narrow_message();
     assert.equal(
@@ -711,7 +730,7 @@ run_test("narrow_to_compose_target streams", ({override_rewire}) => {
     assert.deepEqual(args.operators, [{operator: "stream", operand: "ROME"}]);
 });
 
-run_test("narrow_to_compose_target PMs", ({override, override_rewire}) => {
+run_test("narrow_to_compose_target direct messages", ({override, override_rewire}) => {
     const args = {called: false};
     override_rewire(narrow, "activate", (operators, opts) => {
         args.operators = operators;
@@ -732,7 +751,7 @@ run_test("narrow_to_compose_target PMs", ({override, override_rewire}) => {
     args.called = false;
     narrow.to_compose_target();
     assert.equal(args.called, true);
-    assert.deepEqual(args.operators, [{operator: "pm-with", operand: "alice@example.com"}]);
+    assert.deepEqual(args.operators, [{operator: "dm", operand: "alice@example.com"}]);
 
     // Test with valid persons
     emails = "alice@example.com,ray@example.com";
@@ -740,7 +759,7 @@ run_test("narrow_to_compose_target PMs", ({override, override_rewire}) => {
     narrow.to_compose_target();
     assert.equal(args.called, true);
     assert.deepEqual(args.operators, [
-        {operator: "pm-with", operand: "alice@example.com,ray@example.com"},
+        {operator: "dm", operand: "alice@example.com,ray@example.com"},
     ]);
 
     // Test with some invalid persons
@@ -748,21 +767,21 @@ run_test("narrow_to_compose_target PMs", ({override, override_rewire}) => {
     args.called = false;
     narrow.to_compose_target();
     assert.equal(args.called, true);
-    assert.deepEqual(args.operators, [{operator: "is", operand: "private"}]);
+    assert.deepEqual(args.operators, [{operator: "is", operand: "dm"}]);
 
     // Test with all invalid persons
     emails = "alice,random,ray";
     args.called = false;
     narrow.to_compose_target();
     assert.equal(args.called, true);
-    assert.deepEqual(args.operators, [{operator: "is", operand: "private"}]);
+    assert.deepEqual(args.operators, [{operator: "is", operand: "dm"}]);
 
     // Test with no persons
     emails = "";
     args.called = false;
     narrow.to_compose_target();
     assert.equal(args.called, true);
-    assert.deepEqual(args.operators, [{operator: "is", operand: "private"}]);
+    assert.deepEqual(args.operators, [{operator: "is", operand: "dm"}]);
 });
 
 run_test("narrow_compute_title", ({override}) => {
@@ -803,7 +822,7 @@ run_test("narrow_compute_title", ({override}) => {
     filter = new Filter([{operator: "stream", operand: "Elephant"}]);
     assert.equal(narrow.compute_narrow_title(filter), "translated: Unknown stream #Elephant");
 
-    // Private messages with narrows
+    // Direct messages with narrows
     const joe = {
         email: "joe@example.com",
         user_id: 31,
@@ -811,14 +830,14 @@ run_test("narrow_compute_title", ({override}) => {
     };
     people.add_active_user(joe);
 
-    filter = new Filter([{operator: "pm-with", operand: "joe@example.com"}]);
+    filter = new Filter([{operator: "dm", operand: "joe@example.com"}]);
     assert.equal(narrow.compute_narrow_title(filter), "joe");
 
-    filter = new Filter([{operator: "pm-with", operand: "joe@example.com,sally@doesnotexist.com"}]);
+    filter = new Filter([{operator: "dm", operand: "joe@example.com,sally@doesnotexist.com"}]);
     blueslip.expect("warn", "Unknown emails: joe@example.com,sally@doesnotexist.com");
     assert.equal(narrow.compute_narrow_title(filter), "translated: Invalid users");
 
-    filter = new Filter([{operator: "pm-with", operand: "sally@doesnotexist.com"}]);
+    filter = new Filter([{operator: "dm", operand: "sally@doesnotexist.com"}]);
     blueslip.expect("warn", "Unknown emails: sally@doesnotexist.com");
     assert.equal(narrow.compute_narrow_title(filter), "translated: Invalid user");
 });
diff --git a/web/tests/narrow_local.test.js b/web/tests/narrow_local.test.js
--- a/web/tests/narrow_local.test.js
+++ b/web/tests/narrow_local.test.js
@@ -195,9 +195,9 @@ run_test("is private with no target", () => {
     test_with(fixture);
 });
 
-run_test("pm-with with target outside of range", () => {
+run_test("dm with target outside of range", () => {
     const fixture = {
-        filter_terms: [{operator: "pm-with", operand: "alice@example.com"}],
+        filter_terms: [{operator: "dm", operand: "alice@example.com"}],
         target_id: 5,
         unread_info: {
             flavor: "not_found",
diff --git a/web/tests/narrow_state.test.js b/web/tests/narrow_state.test.js
--- a/web/tests/narrow_state.test.js
+++ b/web/tests/narrow_state.test.js
@@ -79,7 +79,7 @@ test("narrowed", () => {
     assert.ok(narrow_state.narrowed_by_stream_reply());
     assert.ok(!narrow_state.narrowed_to_starred());
 
-    set_filter([["pm-with", "steve@zulip.com"]]);
+    set_filter([["dm", "steve@zulip.com"]]);
     assert.ok(narrow_state.narrowed_to_pms());
     assert.ok(narrow_state.narrowed_by_reply());
     assert.ok(narrow_state.narrowed_by_pm_reply());
@@ -178,9 +178,9 @@ test("set_compose_defaults", () => {
     assert.equal(stream_and_topic.stream, "Foo");
     assert.equal(stream_and_topic.topic, "Bar");
 
-    set_filter([["pm-with", "foo@bar.com"]]);
-    let pm_test = narrow_state.set_compose_defaults();
-    assert.equal(pm_test.private_message_recipient, undefined);
+    set_filter([["dm", "foo@bar.com"]]);
+    let dm_test = narrow_state.set_compose_defaults();
+    assert.equal(dm_test.private_message_recipient, undefined);
 
     const john = {
         email: "john@doe.com",
@@ -190,9 +190,15 @@ test("set_compose_defaults", () => {
     people.add_active_user(john);
     people.add_active_user(john);
 
+    set_filter([["dm", "john@doe.com"]]);
+    dm_test = narrow_state.set_compose_defaults();
+    assert.equal(dm_test.private_message_recipient, "john@doe.com");
+
+    // Even though we renamed "pm-with" to "dm",
+    // compose defaults are set correctly.
     set_filter([["pm-with", "john@doe.com"]]);
-    pm_test = narrow_state.set_compose_defaults();
-    assert.equal(pm_test.private_message_recipient, "john@doe.com");
+    dm_test = narrow_state.set_compose_defaults();
+    assert.equal(dm_test.private_message_recipient, "john@doe.com");
 
     set_filter([
         ["topic", "duplicate"],
@@ -216,13 +222,13 @@ test("update_email", () => {
 
     people.add_active_user(steve);
     set_filter([
-        ["pm-with", "steve@foo.com"],
+        ["dm", "steve@foo.com"],
         ["sender", "steve@foo.com"],
         ["stream", "steve@foo.com"], // try to be tricky
     ]);
     narrow_state.update_email(steve.user_id, "showell@foo.com");
     const filter = narrow_state.filter();
-    assert.deepEqual(filter.operands("pm-with"), ["showell@foo.com"]);
+    assert.deepEqual(filter.operands("dm"), ["showell@foo.com"]);
     assert.deepEqual(filter.operands("sender"), ["showell@foo.com"]);
     assert.deepEqual(filter.operands("stream"), ["steve@foo.com"]);
 });
@@ -245,7 +251,7 @@ test("topic", () => {
 
     set_filter([
         ["sender", "test@foo.com"],
-        ["pm-with", "test@foo.com"],
+        ["dm", "test@foo.com"],
     ]);
     assert.equal(narrow_state.topic(), undefined);
 
@@ -278,8 +284,8 @@ test("stream_sub", () => {
 
 test("pm_ids_string", () => {
     // This function will return undefined unless we're clearly
-    // narrowed to a specific PM (including huddles) with real
-    // users.
+    // narrowed to a specific direct message (including group
+    // direct messages) with real users.
     narrow_state.set_current_filter(undefined);
     assert.equal(narrow_state.pm_ids_string(), undefined);
 
@@ -289,10 +295,10 @@ test("pm_ids_string", () => {
     ]);
     assert.equal(narrow_state.pm_ids_string(), undefined);
 
-    set_filter([["pm-with", ""]]);
+    set_filter([["dm", ""]]);
     assert.equal(narrow_state.pm_ids_string(), undefined);
 
-    set_filter([["pm-with", "bogus@foo.com"]]);
+    set_filter([["dm", "bogus@foo.com"]]);
     assert.equal(narrow_state.pm_ids_string(), undefined);
 
     const alice = {
@@ -310,6 +316,6 @@ test("pm_ids_string", () => {
     people.add_active_user(alice);
     people.add_active_user(bob);
 
-    set_filter([["pm-with", "bob@foo.com,alice@foo.com"]]);
+    set_filter([["dm", "bob@foo.com,alice@foo.com"]]);
     assert.equal(narrow_state.pm_ids_string(), "444,555");
 });
diff --git a/web/tests/narrow_unread.test.js b/web/tests/narrow_unread.test.js
--- a/web/tests/narrow_unread.test.js
+++ b/web/tests/narrow_unread.test.js
@@ -143,7 +143,7 @@ run_test("get_unread_ids", () => {
     // this actually does filtering
     assert_unread_info({flavor: "not_found"});
 
-    terms = [{operator: "pm-with", operand: "alice@example.com"}];
+    terms = [{operator: "dm", operand: "alice@example.com"}];
     set_filter(terms);
     unread_ids = candidate_ids();
     assert.deepEqual(unread_ids, []);
@@ -158,19 +158,25 @@ run_test("get_unread_ids", () => {
         msg_id: private_msg.id,
     });
 
+    // "is:private" was renamed to "is:dm"
     terms = [{operator: "is", operand: "private"}];
     set_filter(terms);
     unread_ids = candidate_ids();
     assert.deepEqual(unread_ids, [private_msg.id]);
 
+    terms = [{operator: "is", operand: "dm"}];
+    set_filter(terms);
+    unread_ids = candidate_ids();
+    assert.deepEqual(unread_ids, [private_msg.id]);
+
     // For a negated search, our candidate ids will be all
     // unread messages, even ones that don't pass the filter.
-    terms = [{operator: "is", operand: "private", negated: true}];
+    terms = [{operator: "is", operand: "dm", negated: true}];
     set_filter(terms);
     unread_ids = candidate_ids();
     assert.deepEqual(unread_ids, [stream_msg.id, private_msg.id]);
 
-    terms = [{operator: "pm-with", operand: "bob@example.com"}];
+    terms = [{operator: "dm", operand: "bob@example.com"}];
     set_filter(terms);
 
     unread_ids = candidate_ids();
diff --git a/web/tests/people.test.js b/web/tests/people.test.js
--- a/web/tests/people.test.js
+++ b/web/tests/people.test.js
@@ -771,8 +771,8 @@ test_people("message_methods", () => {
         display_recipient: [{id: maria.user_id}, {id: me.user_id}, {id: charles.user_id}],
         sender_id: charles.user_id,
     };
-    assert.equal(people.pm_with_url(message), "#narrow/pm-with/301,302-group");
-    assert.equal(people.pm_perma_link(message), "#narrow/pm-with/30,301,302-group");
+    assert.equal(people.pm_with_url(message), "#narrow/dm/301,302-group");
+    assert.equal(people.pm_perma_link(message), "#narrow/dm/30,301,302-group");
     assert.equal(people.pm_reply_to(message), "Athens@example.com,charles@example.com");
     assert.equal(people.small_avatar_url(message), "http://charles.com/foo.png?s=50");
 
@@ -781,8 +781,8 @@ test_people("message_methods", () => {
         display_recipient: [{id: maria.user_id}, {id: me.user_id}],
         avatar_url: "legacy.png",
     };
-    assert.equal(people.pm_with_url(message), "#narrow/pm-with/302-Maria-Athens");
-    assert.equal(people.pm_perma_link(message), "#narrow/pm-with/30,302-pm");
+    assert.equal(people.pm_with_url(message), "#narrow/dm/302-Maria-Athens");
+    assert.equal(people.pm_perma_link(message), "#narrow/dm/30,302-dm");
     assert.equal(people.pm_reply_to(message), "Athens@example.com");
     assert.equal(people.small_avatar_url(message), "http://zulip.zulipdev.com/legacy.png?s=50");
 
@@ -818,8 +818,8 @@ test_people("message_methods", () => {
         type: "private",
         display_recipient: [{id: me.user_id}],
     };
-    assert.equal(people.pm_with_url(message), "#narrow/pm-with/30-Me-Myself");
-    assert.equal(people.pm_perma_link(message), "#narrow/pm-with/30-pm");
+    assert.equal(people.pm_with_url(message), "#narrow/dm/30-Me-Myself");
+    assert.equal(people.pm_perma_link(message), "#narrow/dm/30-dm");
 
     message = {type: "stream"};
     assert.equal(people.pm_with_user_ids(message), undefined);
diff --git a/web/tests/people_errors.test.js b/web/tests/people_errors.test.js
--- a/web/tests/people_errors.test.js
+++ b/web/tests/people_errors.test.js
@@ -75,9 +75,8 @@ run_test("blueslip", () => {
         display_recipient: [],
         sender_id: me.user_id,
     };
-    blueslip.expect("error", "Empty recipient list in message", 4);
+    blueslip.expect("error", "Empty recipient list in message", 3);
     people.pm_with_user_ids(message);
-    people.group_pm_with_user_ids(message);
     people.all_user_ids_in_pm(message);
     assert.equal(people.pm_perma_link(message), undefined);
 
diff --git a/web/tests/pm_list_data.test.js b/web/tests/pm_list_data.test.js
--- a/web/tests/pm_list_data.test.js
+++ b/web/tests/pm_list_data.test.js
@@ -68,7 +68,7 @@ function test(label, f) {
 }
 
 function set_pm_with_filter(emails) {
-    const active_filter = new Filter([{operator: "pm-with", operand: emails}]);
+    const active_filter = new Filter([{operator: "dm", operand: emails}]);
     narrow_state.set_current_filter(active_filter);
 }
 
@@ -96,7 +96,7 @@ test("get_conversations", ({override}) => {
             is_zero: false,
             recipients: "Me Myself",
             unread: 1,
-            url: "#narrow/pm-with/103-Me-Myself",
+            url: "#narrow/dm/103-Me-Myself",
             user_circle_class: "user_circle_empty",
             user_ids_string: "103",
             status_emoji_info: {
@@ -109,7 +109,7 @@ test("get_conversations", ({override}) => {
             unread: 1,
             is_zero: false,
             is_active: false,
-            url: "#narrow/pm-with/101,102-group",
+            url: "#narrow/dm/101,102-group",
             user_circle_class: undefined,
             is_group: true,
             status_emoji_info: undefined,
@@ -147,7 +147,7 @@ test("get_conversations bot", ({override}) => {
             unread: 1,
             is_zero: false,
             is_active: false,
-            url: "#narrow/pm-with/314-Outgoing-webhook",
+            url: "#narrow/dm/314-Outgoing-webhook",
             status_emoji_info: undefined,
             user_circle_class: "user_circle_green",
             is_group: false,
@@ -158,7 +158,7 @@ test("get_conversations bot", ({override}) => {
             unread: 1,
             is_zero: false,
             is_active: false,
-            url: "#narrow/pm-with/101,102-group",
+            url: "#narrow/dm/101,102-group",
             user_circle_class: undefined,
             status_emoji_info: undefined,
             is_group: true,
diff --git a/web/tests/popovers.test.js b/web/tests/popovers.test.js
--- a/web/tests/popovers.test.js
+++ b/web/tests/popovers.test.js
@@ -176,7 +176,7 @@ test_ui("sender_hover", ({override, mock_template}) => {
             user_type: $t({defaultMessage: "Member"}),
             user_circle_class: "user_circle_empty",
             user_last_seen_time_status: "translated: Active more than 2 weeks ago",
-            pm_with_url: "#narrow/pm-with/42-Alice-Smith",
+            pm_with_url: "#narrow/dm/42-Alice-Smith",
             sent_by_url: "#narrow/sender/42-Alice-Smith",
             private_message_class: "respond_personal_button",
             show_manage_menu: true,
diff --git a/web/tests/search_future.test.js b/web/tests/search_future.test.js
--- a/web/tests/search_future.test.js
+++ b/web/tests/search_future.test.js
@@ -151,11 +151,11 @@ test("initialize", ({mock_template}) => {
             const search_suggestions = {
                 lookup_table: new Map([
                     [
-                        "group-pm-with:zo",
+                        "dm-including:zo",
                         {
                             description_html: "group direct messages including",
                             is_person: true,
-                            search_string: "group-pm-with:user7@zulipdev.com",
+                            search_string: "dm-including:user7@zulipdev.com",
                             user_pill_context: {
                                 display_value: "<strong>Zo</strong>e",
                                 has_image: true,
@@ -166,11 +166,11 @@ test("initialize", ({mock_template}) => {
                         },
                     ],
                     [
-                        "pm-with:zo",
+                        "dm:zo",
                         {
                             description_html: "direct messages with",
                             is_person: true,
-                            search_string: "pm-with:user7@zulipdev.com",
+                            search_string: "dm:user7@zulipdev.com",
                             user_pill_context: {
                                 display_value: "<strong>Zo</strong>e",
                                 has_image: true,
@@ -203,7 +203,7 @@ test("initialize", ({mock_template}) => {
                         },
                     ],
                 ]),
-                strings: ["zo", "sender:zo", "pm-with:zo", "group-pm-with:zo"],
+                strings: ["zo", "sender:zo", "dm:zo", "dm-including:zo"],
             };
 
             /* Test source */
diff --git a/web/tests/search_now.test.js b/web/tests/search_now.test.js
--- a/web/tests/search_now.test.js
+++ b/web/tests/search_now.test.js
@@ -133,11 +133,11 @@ run_test("initialize", ({mock_template}) => {
             const search_suggestions = {
                 lookup_table: new Map([
                     [
-                        "group-pm-with:zo",
+                        "dm-including:zo",
                         {
                             description_html: "group direct messages including",
                             is_person: true,
-                            search_string: "group-pm-with:user7@zulipdev.com",
+                            search_string: "dm-including:user7@zulipdev.com",
                             user_pill_context: {
                                 display_value: "<strong>Zo</strong>e",
                                 has_image: true,
@@ -148,11 +148,11 @@ run_test("initialize", ({mock_template}) => {
                         },
                     ],
                     [
-                        "pm-with:zo",
+                        "dm:zo",
                         {
                             description_html: "direct messages with",
                             is_person: true,
-                            search_string: "pm-with:user7@zulipdev.com",
+                            search_string: "dm:user7@zulipdev.com",
                             user_pill_context: {
                                 display_value: "<strong>Zo</strong>e",
                                 has_image: true,
@@ -185,7 +185,7 @@ run_test("initialize", ({mock_template}) => {
                         },
                     ],
                 ]),
-                strings: ["zo", "sender:zo", "pm-with:zo", "group-pm-with:zo"],
+                strings: ["zo", "sender:zo", "dm:zo", "dm-including:zo"],
             };
 
             /* Test source */
diff --git a/web/tests/search_suggestion_future.test.js b/web/tests/search_suggestion_future.test.js
--- a/web/tests/search_suggestion_future.test.js
+++ b/web/tests/search_suggestion_future.test.js
@@ -112,44 +112,57 @@ test("subset_suggestions", () => {
     assert.deepEqual(suggestions.strings, expected);
 });
 
-test("private_suggestions", () => {
-    let query = "is:private";
+test("dm_suggestions", ({override}) => {
+    let query = "is:dm";
     let suggestions = get_suggestions("", query);
     let expected = [
-        "is:private",
-        "pm-with:alice@zulip.com",
-        "pm-with:bob@zulip.com",
-        "pm-with:jeff@zulip.com",
-        "pm-with:myself@zulip.com",
-        "pm-with:ted@zulip.com",
+        "is:dm",
+        "dm:alice@zulip.com",
+        "dm:bob@zulip.com",
+        "dm:jeff@zulip.com",
+        "dm:myself@zulip.com",
+        "dm:ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "al";
-    let base_query = "is:private";
+    let base_query = "is:dm";
     suggestions = get_suggestions(base_query, query);
     expected = [
         "al",
         "is:alerted",
         "sender:alice@zulip.com",
-        "pm-with:alice@zulip.com",
-        "group-pm-with:alice@zulip.com",
+        "dm:alice@zulip.com",
+        "dm-including:alice@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:t";
+    // "is:private" was renamed to "is:dm", so
+    // we suggest "is:dm" to anyone with "is:private"
+    // in their muscle memory.
+    query = "is:pr";
+    suggestions = get_suggestions("", query);
+    expected = ["is:dm"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    query = "is:private";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:t", "pm-with:ted@zulip.com"];
+    expected = ["is:dm"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:t";
+    query = "dm:t";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:t", "is:private -pm-with:ted@zulip.com"];
+    expected = ["dm:t", "dm:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:ted@zulip.com";
+    query = "-dm:t";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:ted@zulip.com"];
+    expected = ["-dm:t", "is:dm -dm:ted@zulip.com"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    query = "dm:ted@zulip.com";
+    suggestions = get_suggestions("", query);
+    expected = ["dm:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "sender:ted";
@@ -181,13 +194,13 @@ test("private_suggestions", () => {
     // Users can enter bizarre queries, and if they do, we want to
     // be conservative with suggestions.
     query = "near:3";
-    base_query = "is:private";
+    base_query = "is:dm";
     suggestions = get_suggestions(base_query, query);
     expected = ["near:3"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "near:3";
-    base_query = "pm-with:ted@zulip.com";
+    base_query = "dm:ted@zulip.com";
     suggestions = get_suggestions(base_query, query);
     expected = ["near:3"];
     assert.deepEqual(suggestions.strings, expected);
@@ -200,22 +213,22 @@ test("private_suggestions", () => {
     assert.deepEqual(suggestions.strings, expected);
 
     query = "al";
-    base_query = "is:starred has:link is:private";
+    base_query = "is:starred has:link is:dm";
     suggestions = get_suggestions(base_query, query);
     expected = [
         "al",
         "is:alerted",
         "sender:alice@zulip.com",
-        "pm-with:alice@zulip.com",
-        "group-pm-with:alice@zulip.com",
+        "dm:alice@zulip.com",
+        "dm-including:alice@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
     // Make sure it handles past context correctly
-    query = "pm-with:";
+    query = "dm:";
     base_query = "stream:Denmark";
     suggestions = get_suggestions(base_query, query);
-    expected = ["pm-with:"];
+    expected = ["dm:"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "sender:";
@@ -223,85 +236,111 @@ test("private_suggestions", () => {
     suggestions = get_suggestions(base_query, query);
     expected = ["sender:"];
     assert.deepEqual(suggestions.strings, expected);
+
+    // "pm-with" operator returns search result
+    // and "dm" operator as a suggestions
+    override(narrow_state, "stream", () => undefined);
+    query = "pm-with";
+    suggestions = get_suggestions("", query);
+    expected = ["pm-with", "dm:"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    query = "pm-with:t";
+    suggestions = get_suggestions("", query);
+    expected = ["pm-with:t", "dm:ted@zulip.com"];
+    assert.deepEqual(suggestions.strings, expected);
 });
 
 test("group_suggestions", () => {
-    // Entering a comma in a pm-with query should immediately generate
+    // Entering a comma in a dm query should immediately generate
     // suggestions for the next person.
-    let query = "pm-with:bob@zulip.com,";
+    let query = "dm:bob@zulip.com,";
     let suggestions = get_suggestions("", query);
     let expected = [
-        "pm-with:bob@zulip.com,",
-        "pm-with:bob@zulip.com,alice@zulip.com",
-        "pm-with:bob@zulip.com,jeff@zulip.com",
-        "pm-with:bob@zulip.com,ted@zulip.com",
+        "dm:bob@zulip.com,",
+        "dm:bob@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    // Only the last part of a comma-separated pm-with query should be used to
+    // Only the last part of a comma-separated dm query should be used to
     // generate suggestions.
-    query = "pm-with:bob@zulip.com,t";
+    query = "dm:bob@zulip.com,t";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:bob@zulip.com,t", "pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["dm:bob@zulip.com,t", "dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     // Smit should also generate ted@zulip.com (Ted Smith) as a suggestion.
-    query = "pm-with:bob@zulip.com,Smit";
+    query = "dm:bob@zulip.com,Smit";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:bob@zulip.com,Smit", "pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["dm:bob@zulip.com,Smit", "dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     // Do not suggest "myself@zulip.com" (the name of the current user)
-    query = "pm-with:ted@zulip.com,mys";
+    query = "dm:ted@zulip.com,mys";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:ted@zulip.com,mys"];
+    expected = ["dm:ted@zulip.com,mys"];
     assert.deepEqual(suggestions.strings, expected);
 
     // No superfluous suggestions should be generated.
-    query = "pm-with:bob@zulip.com,red";
+    query = "dm:bob@zulip.com,red";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:bob@zulip.com,red"];
+    expected = ["dm:bob@zulip.com,red"];
     assert.deepEqual(suggestions.strings, expected);
 
-    // is:private should be properly prepended to each suggestion if the pm-with
-    // operator is negated.
+    // "is:dm" should be properly prepended to each suggestion
+    // if the "dm" operator is negated.
 
-    query = "-pm-with:bob@zulip.com,";
+    query = "-dm:bob@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "-pm-with:bob@zulip.com,",
-        "is:private -pm-with:bob@zulip.com,alice@zulip.com",
-        "is:private -pm-with:bob@zulip.com,jeff@zulip.com",
-        "is:private -pm-with:bob@zulip.com,ted@zulip.com",
+        "-dm:bob@zulip.com,",
+        "is:dm -dm:bob@zulip.com,alice@zulip.com",
+        "is:dm -dm:bob@zulip.com,jeff@zulip.com",
+        "is:dm -dm:bob@zulip.com,ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:bob@zulip.com,t";
+    query = "-dm:bob@zulip.com,t";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:bob@zulip.com,t", "is:private -pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["-dm:bob@zulip.com,t", "is:dm -dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:bob@zulip.com,Smit";
+    query = "-dm:bob@zulip.com,Smit";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:bob@zulip.com,Smit", "is:private -pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["-dm:bob@zulip.com,Smit", "is:dm -dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:bob@zulip.com,red";
+    query = "-dm:bob@zulip.com,red";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:bob@zulip.com,red"];
+    expected = ["-dm:bob@zulip.com,red"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    // If user types "pm-with" operator, an email and a comma,
+    // show suggestions for group direct messages with the "dm"
+    // operator.
+    query = "pm-with:bob@zulip.com,";
+    suggestions = get_suggestions("", query);
+    expected = [
+        "pm-with:bob@zulip.com,",
+        "dm:bob@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com",
+    ];
     assert.deepEqual(suggestions.strings, expected);
 
     // Test multiple operators
-    query = "pm-with:bob@zulip.com,Smit";
+    query = "dm:bob@zulip.com,Smit";
     let base_query = "is:starred has:link";
     suggestions = get_suggestions(base_query, query);
-    expected = ["pm-with:bob@zulip.com,Smit", "pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["dm:bob@zulip.com,Smit", "dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:bob@zulip.com,Smit";
+    query = "dm:bob@zulip.com,Smit";
     base_query = "stream:Denmark has:link";
     suggestions = get_suggestions(base_query, query);
-    expected = ["pm-with:bob@zulip.com,Smit"];
+    expected = ["dm:bob@zulip.com,Smit"];
     assert.deepEqual(suggestions.strings, expected);
 
     function message(user_ids, timestamp) {
@@ -319,42 +358,45 @@ test("group_suggestions", () => {
         message([bob.user_id, ted.user_id, jeff.user_id], 98),
     ]);
 
-    // Simulate a past huddle which should now prioritize ted over alice
-    query = "pm-with:bob@zulip.com,";
+    // Simulate a past group direct message which should now
+    // prioritize ted over alice
+    query = "dm:bob@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "pm-with:bob@zulip.com,",
-        "pm-with:bob@zulip.com,ted@zulip.com",
-        "pm-with:bob@zulip.com,alice@zulip.com",
-        "pm-with:bob@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,",
+        "dm:bob@zulip.com,ted@zulip.com",
+        "dm:bob@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,jeff@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    // bob,ted,jeff is already an existing huddle, so prioritize this one
-    query = "pm-with:bob@zulip.com,ted@zulip.com,";
+    // bob, ted, and jeff are already an existing group direct message,
+    // so prioritize this one
+    query = "dm:bob@zulip.com,ted@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "pm-with:bob@zulip.com,ted@zulip.com,",
-        "pm-with:bob@zulip.com,ted@zulip.com,jeff@zulip.com",
-        "pm-with:bob@zulip.com,ted@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com,",
+        "dm:bob@zulip.com,ted@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com,alice@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    // bob,ted,jeff is already an existing huddle, but if we start with just jeff,
-    // then don't prioritize ted over alice because it doesn't complete the full huddle.
-    query = "pm-with:jeff@zulip.com,";
+    // bob, ted, and jeff are already an existing group direct message,
+    // but if we start with just jeff, then don't prioritize ted over
+    // alice because it doesn't complete the full group direct message.
+    query = "dm:jeff@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "pm-with:jeff@zulip.com,",
-        "pm-with:jeff@zulip.com,alice@zulip.com",
-        "pm-with:jeff@zulip.com,bob@zulip.com",
-        "pm-with:jeff@zulip.com,ted@zulip.com",
+        "dm:jeff@zulip.com,",
+        "dm:jeff@zulip.com,alice@zulip.com",
+        "dm:jeff@zulip.com,bob@zulip.com",
+        "dm:jeff@zulip.com,ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:jeff@zulip.com,ted@zulip.com hi";
+    query = "dm:jeff@zulip.com,ted@zulip.com hi";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:jeff@zulip.com,ted@zulip.com hi"];
+    expected = ["dm:jeff@zulip.com,ted@zulip.com hi"];
     assert.deepEqual(suggestions.strings, expected);
 });
 
@@ -369,7 +411,7 @@ test("empty_query_suggestions", () => {
     const expected = [
         "",
         "streams:public",
-        "is:private",
+        "is:dm",
         "is:starred",
         "is:mentioned",
         "is:alerted",
@@ -388,7 +430,7 @@ test("empty_query_suggestions", () => {
     function describe(q) {
         return suggestions.lookup_table.get(q).description_html;
     }
-    assert.equal(describe("is:private"), "Direct messages");
+    assert.equal(describe("is:dm"), "Direct messages");
     assert.equal(describe("is:starred"), "Starred messages");
     assert.equal(describe("is:mentioned"), "@-mentions");
     assert.equal(describe("is:alerted"), "Alerted messages");
@@ -466,15 +508,15 @@ test("check_is_suggestions", ({override}) => {
     let suggestions = get_suggestions("", query);
     let expected = [
         "i",
-        "is:private",
+        "is:dm",
         "is:starred",
         "is:mentioned",
         "is:alerted",
         "is:unread",
         "is:resolved",
         "sender:alice@zulip.com",
-        "pm-with:alice@zulip.com",
-        "group-pm-with:alice@zulip.com",
+        "dm:alice@zulip.com",
+        "dm-including:alice@zulip.com",
         "has:image",
     ];
     assert.deepEqual(suggestions.strings, expected);
@@ -483,7 +525,7 @@ test("check_is_suggestions", ({override}) => {
         return suggestions.lookup_table.get(q).description_html;
     }
 
-    assert.equal(describe("is:private"), "Direct messages");
+    assert.equal(describe("is:dm"), "Direct messages");
     assert.equal(describe("is:starred"), "Starred messages");
     assert.equal(describe("is:mentioned"), "@-mentions");
     assert.equal(describe("is:alerted"), "Alerted messages");
@@ -494,7 +536,7 @@ test("check_is_suggestions", ({override}) => {
     suggestions = get_suggestions("", query);
     expected = [
         "-i",
-        "-is:private",
+        "-is:dm",
         "-is:starred",
         "-is:mentioned",
         "-is:alerted",
@@ -503,7 +545,7 @@ test("check_is_suggestions", ({override}) => {
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    assert.equal(describe("-is:private"), "Exclude direct messages");
+    assert.equal(describe("-is:dm"), "Exclude direct messages");
     assert.equal(describe("-is:starred"), "Exclude starred messages");
     assert.equal(describe("-is:mentioned"), "Exclude @-mentions");
     assert.equal(describe("-is:alerted"), "Exclude alerted messages");
@@ -515,7 +557,7 @@ test("check_is_suggestions", ({override}) => {
     expected = [
         "",
         "streams:public",
-        "is:private",
+        "is:dm",
         "is:starred",
         "is:mentioned",
         "is:alerted",
@@ -531,7 +573,7 @@ test("check_is_suggestions", ({override}) => {
     assert.deepEqual(suggestions.strings, expected);
 
     query = "";
-    let base_query = "is:private";
+    let base_query = "is:dm";
     suggestions = get_suggestions(base_query, query);
     expected = [
         "is:starred",
@@ -549,14 +591,7 @@ test("check_is_suggestions", ({override}) => {
 
     query = "is:";
     suggestions = get_suggestions("", query);
-    expected = [
-        "is:private",
-        "is:starred",
-        "is:mentioned",
-        "is:alerted",
-        "is:unread",
-        "is:resolved",
-    ];
+    expected = ["is:dm", "is:starred", "is:mentioned", "is:alerted", "is:unread", "is:resolved"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "is:st";
@@ -672,12 +707,7 @@ test("topic_suggestions", ({override}) => {
     stream_data.add_sub({stream_id: office_id, name: "office", subscribed: true});
 
     suggestions = get_suggestions("", "te");
-    expected = [
-        "te",
-        "sender:ted@zulip.com",
-        "pm-with:ted@zulip.com",
-        "group-pm-with:ted@zulip.com",
-    ];
+    expected = ["te", "sender:ted@zulip.com", "dm:ted@zulip.com", "dm-including:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     stream_topic_history.add_message({
@@ -696,8 +726,8 @@ test("topic_suggestions", ({override}) => {
     expected = [
         "te",
         "sender:ted@zulip.com",
-        "pm-with:ted@zulip.com",
-        "group-pm-with:ted@zulip.com",
+        "dm:ted@zulip.com",
+        "dm-including:ted@zulip.com",
         "stream:office topic:team",
         "stream:office topic:test",
     ];
@@ -729,7 +759,7 @@ test("topic_suggestions", ({override}) => {
     expected = ["topic:", "topic:REXX"];
     assert.deepEqual(suggestions.strings, expected);
 
-    suggestions = get_suggestions("is:private stream:devel", "topic:");
+    suggestions = get_suggestions("is:dm stream:devel", "topic:");
     expected = ["topic:"];
     assert.deepEqual(suggestions.strings, expected);
 
@@ -850,51 +880,46 @@ test("people_suggestions", ({override}) => {
         "te",
         "sender:bob@zulip.com",
         "sender:ted@zulip.com",
-        "pm-with:bob@zulip.com", // bob térry
-        "pm-with:ted@zulip.com",
-        "group-pm-with:bob@zulip.com",
-        "group-pm-with:ted@zulip.com",
+        "dm:bob@zulip.com", // bob térry
+        "dm:ted@zulip.com",
+        "dm-including:bob@zulip.com",
+        "dm-including:ted@zulip.com",
     ];
 
     assert.deepEqual(suggestions.strings, expected);
 
     const is_person = (q) => suggestions.lookup_table.get(q).is_person;
-    assert.equal(is_person("pm-with:ted@zulip.com"), true);
+    assert.equal(is_person("dm:ted@zulip.com"), true);
     assert.equal(is_person("sender:ted@zulip.com"), true);
-    assert.equal(is_person("group-pm-with:ted@zulip.com"), true);
+    assert.equal(is_person("dm-including:ted@zulip.com"), true);
 
     const has_image = (q) => suggestions.lookup_table.get(q).user_pill_context.has_image;
-    assert.equal(has_image("pm-with:bob@zulip.com"), true);
+    assert.equal(has_image("dm:bob@zulip.com"), true);
     assert.equal(has_image("sender:bob@zulip.com"), true);
-    assert.equal(has_image("group-pm-with:bob@zulip.com"), true);
+    assert.equal(has_image("dm-including:bob@zulip.com"), true);
 
     const describe = (q) => suggestions.lookup_table.get(q).description_html;
-    assert.equal(describe("pm-with:ted@zulip.com"), "Direct messages with");
+    assert.equal(describe("dm:ted@zulip.com"), "Direct messages with");
     assert.equal(describe("sender:ted@zulip.com"), "Sent by");
-    assert.equal(describe("group-pm-with:ted@zulip.com"), "Group direct messages including");
+    assert.equal(describe("dm-including:ted@zulip.com"), "Direct messages including");
 
     let expectedString = "<strong>Te</strong>d Smith";
 
     const get_full_name = (q) =>
         suggestions.lookup_table.get(q).user_pill_context.display_value.string;
     assert.equal(get_full_name("sender:ted@zulip.com"), expectedString);
-    assert.equal(get_full_name("pm-with:ted@zulip.com"), expectedString);
-    assert.equal(get_full_name("group-pm-with:ted@zulip.com"), expectedString);
+    assert.equal(get_full_name("dm:ted@zulip.com"), expectedString);
+    assert.equal(get_full_name("dm-including:ted@zulip.com"), expectedString);
 
     expectedString = `${example_avatar_url}?s=50`;
 
     const get_avatar_url = (q) => suggestions.lookup_table.get(q).user_pill_context.img_src;
-    assert.equal(get_avatar_url("pm-with:bob@zulip.com"), expectedString);
+    assert.equal(get_avatar_url("dm:bob@zulip.com"), expectedString);
     assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString);
-    assert.equal(get_avatar_url("group-pm-with:bob@zulip.com"), expectedString);
+    assert.equal(get_avatar_url("dm-including:bob@zulip.com"), expectedString);
 
     suggestions = get_suggestions("", "Ted "); // note space
-    expected = [
-        "Ted",
-        "sender:ted@zulip.com",
-        "pm-with:ted@zulip.com",
-        "group-pm-with:ted@zulip.com",
-    ];
+    expected = ["Ted", "sender:ted@zulip.com", "dm:ted@zulip.com", "dm-including:ted@zulip.com"];
 
     assert.deepEqual(suggestions.strings, expected);
 
@@ -937,11 +962,6 @@ test("operator_suggestions", ({override}) => {
     expected = ["st", "streams:public", "is:starred", "stream:"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "group-";
-    suggestions = get_suggestions("", query);
-    expected = ["group-", "group-pm-with:"];
-    assert.deepEqual(suggestions.strings, expected);
-
     query = "-s";
     suggestions = get_suggestions("", query);
     expected = ["-s", "-streams:public", "-sender:myself@zulip.com", "-stream:", "-sender:"];
@@ -980,15 +1000,15 @@ test("queries_with_spaces", () => {
 // When input search query contains multiple operators
 // and a pill hasn't been formed from those operators.
 test("multiple_operators_without_pills", () => {
-    let query = "is:private al";
+    let query = "is:dm al";
     let base_query = "";
     let suggestions = get_suggestions(base_query, query);
     let expected = [
-        "is:private al",
-        "is:private is:alerted",
-        "is:private sender:alice@zulip.com",
-        "is:private pm-with:alice@zulip.com",
-        "is:private group-pm-with:alice@zulip.com",
+        "is:dm al",
+        "is:dm is:alerted",
+        "is:dm sender:alice@zulip.com",
+        "is:dm dm:alice@zulip.com",
+        "is:dm dm-including:alice@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
diff --git a/web/tests/search_suggestion_now.test.js b/web/tests/search_suggestion_now.test.js
--- a/web/tests/search_suggestion_now.test.js
+++ b/web/tests/search_suggestion_now.test.js
@@ -114,44 +114,57 @@ test("subset_suggestions", () => {
     assert.deepEqual(suggestions.strings, expected);
 });
 
-test("private_suggestions", () => {
-    let query = "is:private";
+test("dm_suggestions", ({override}) => {
+    let query = "is:dm";
     let suggestions = get_suggestions("", query);
     let expected = [
-        "is:private",
-        "pm-with:alice@zulip.com",
-        "pm-with:bob@zulip.com",
-        "pm-with:jeff@zulip.com",
-        "pm-with:myself@zulip.com",
-        "pm-with:ted@zulip.com",
+        "is:dm",
+        "dm:alice@zulip.com",
+        "dm:bob@zulip.com",
+        "dm:jeff@zulip.com",
+        "dm:myself@zulip.com",
+        "dm:ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "is:private al";
+    query = "is:dm al";
     suggestions = get_suggestions("", query);
     expected = [
-        "is:private al",
-        "is:private is:alerted",
-        "is:private sender:alice@zulip.com",
-        "is:private pm-with:alice@zulip.com",
-        "is:private group-pm-with:alice@zulip.com",
-        "is:private",
+        "is:dm al",
+        "is:dm is:alerted",
+        "is:dm sender:alice@zulip.com",
+        "is:dm dm:alice@zulip.com",
+        "is:dm dm-including:alice@zulip.com",
+        "is:dm",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:t";
+    // "is:private" was renamed to "is:dm", so
+    // we suggest "is:dm" to anyone with "is:private"
+    // in their muscle memory.
+    query = "is:pr";
+    suggestions = get_suggestions("", query);
+    expected = ["is:dm"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    query = "is:private";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:t", "pm-with:ted@zulip.com"];
+    expected = ["is:dm"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:t";
+    query = "dm:t";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:t", "is:private -pm-with:ted@zulip.com"];
+    expected = ["dm:t", "dm:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:ted@zulip.com";
+    query = "-dm:t";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:ted@zulip.com"];
+    expected = ["-dm:t", "is:dm -dm:ted@zulip.com"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    query = "dm:ted@zulip.com";
+    suggestions = get_suggestions("", query);
+    expected = ["dm:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "sender:ted";
@@ -181,14 +194,14 @@ test("private_suggestions", () => {
 
     // Users can enter bizarre queries, and if they do, we want to
     // be conservative with suggestions.
-    query = "is:private near:3";
+    query = "is:dm near:3";
     suggestions = get_suggestions("", query);
-    expected = ["is:private near:3", "is:private"];
+    expected = ["is:dm near:3", "is:dm"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:ted@zulip.com near:3";
+    query = "dm:ted@zulip.com near:3";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:ted@zulip.com near:3", "pm-with:ted@zulip.com"];
+    expected = ["dm:ted@zulip.com near:3", "dm:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     // Make sure suggestions still work if preceding tokens
@@ -197,113 +210,139 @@ test("private_suggestions", () => {
     expected = ["is:alerted sender:ted@zulip.com", "is:alerted"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "is:starred has:link is:private al";
+    query = "is:starred has:link is:dm al";
     suggestions = get_suggestions("", query);
     expected = [
-        "is:starred has:link is:private al",
-        "is:starred has:link is:private is:alerted",
-        "is:starred has:link is:private sender:alice@zulip.com",
-        "is:starred has:link is:private pm-with:alice@zulip.com",
-        "is:starred has:link is:private group-pm-with:alice@zulip.com",
-        "is:starred has:link is:private",
+        "is:starred has:link is:dm al",
+        "is:starred has:link is:dm is:alerted",
+        "is:starred has:link is:dm sender:alice@zulip.com",
+        "is:starred has:link is:dm dm:alice@zulip.com",
+        "is:starred has:link is:dm dm-including:alice@zulip.com",
+        "is:starred has:link is:dm",
         "is:starred has:link",
         "is:starred",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
     // Make sure it handles past context correctly
-    query = "stream:Denmark pm-with:";
+    query = "stream:Denmark dm:";
     suggestions = get_suggestions("", query);
-    expected = ["stream:Denmark pm-with:", "stream:Denmark"];
+    expected = ["stream:Denmark dm:", "stream:Denmark"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "sender:ted@zulip.com sender:";
     suggestions = get_suggestions("", query);
     expected = ["sender:ted@zulip.com sender:", "sender:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
+
+    // "pm-with" operator returns search result
+    // and "dm" operator as a suggestions
+    override(narrow_state, "stream", () => undefined);
+    query = "pm-with";
+    suggestions = get_suggestions("", query);
+    expected = ["pm-with", "dm:"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    query = "pm-with:t";
+    suggestions = get_suggestions("", query);
+    expected = ["pm-with:t", "dm:ted@zulip.com"];
+    assert.deepEqual(suggestions.strings, expected);
 });
 
 test("group_suggestions", () => {
-    // Entering a comma in a pm-with query should immediately generate
-    // suggestions for the next person.
-    let query = "pm-with:bob@zulip.com,";
+    // Entering a comma in a "dm:" query should immediately
+    // generate suggestions for the next person.
+    let query = "dm:bob@zulip.com,";
     let suggestions = get_suggestions("", query);
     let expected = [
-        "pm-with:bob@zulip.com,",
-        "pm-with:bob@zulip.com,alice@zulip.com",
-        "pm-with:bob@zulip.com,jeff@zulip.com",
-        "pm-with:bob@zulip.com,ted@zulip.com",
+        "dm:bob@zulip.com,",
+        "dm:bob@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    // Only the last part of a comma-separated pm-with query should be used to
-    // generate suggestions.
-    query = "pm-with:bob@zulip.com,t";
+    // Only the last part of a comma-separated "dm" query
+    // should be used to generate suggestions.
+    query = "dm:bob@zulip.com,t";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:bob@zulip.com,t", "pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["dm:bob@zulip.com,t", "dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     // Smit should also generate ted@zulip.com (Ted Smith) as a suggestion.
-    query = "pm-with:bob@zulip.com,Smit";
+    query = "dm:bob@zulip.com,Smit";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:bob@zulip.com,Smit", "pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["dm:bob@zulip.com,Smit", "dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     // Do not suggest "myself@zulip.com" (the name of the current user)
-    query = "pm-with:ted@zulip.com,my";
+    query = "dm:ted@zulip.com,my";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:ted@zulip.com,my"];
+    expected = ["dm:ted@zulip.com,my"];
     assert.deepEqual(suggestions.strings, expected);
 
     // No superfluous suggestions should be generated.
-    query = "pm-with:bob@zulip.com,red";
+    query = "dm:bob@zulip.com,red";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:bob@zulip.com,red"];
+    expected = ["dm:bob@zulip.com,red"];
     assert.deepEqual(suggestions.strings, expected);
 
-    // is:private should be properly prepended to each suggestion if the pm-with
-    // operator is negated.
+    // "is:dm" should be properly prepended to each suggestion
+    // if the "dm" operator is negated.
 
-    query = "-pm-with:bob@zulip.com,";
+    query = "-dm:bob@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "-pm-with:bob@zulip.com,",
-        "is:private -pm-with:bob@zulip.com,alice@zulip.com",
-        "is:private -pm-with:bob@zulip.com,jeff@zulip.com",
-        "is:private -pm-with:bob@zulip.com,ted@zulip.com",
+        "-dm:bob@zulip.com,",
+        "is:dm -dm:bob@zulip.com,alice@zulip.com",
+        "is:dm -dm:bob@zulip.com,jeff@zulip.com",
+        "is:dm -dm:bob@zulip.com,ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:bob@zulip.com,t";
+    query = "-dm:bob@zulip.com,t";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:bob@zulip.com,t", "is:private -pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["-dm:bob@zulip.com,t", "is:dm -dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:bob@zulip.com,Smit";
+    query = "-dm:bob@zulip.com,Smit";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:bob@zulip.com,Smit", "is:private -pm-with:bob@zulip.com,ted@zulip.com"];
+    expected = ["-dm:bob@zulip.com,Smit", "is:dm -dm:bob@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "-pm-with:bob@zulip.com,red";
+    query = "-dm:bob@zulip.com,red";
     suggestions = get_suggestions("", query);
-    expected = ["-pm-with:bob@zulip.com,red"];
+    expected = ["-dm:bob@zulip.com,red"];
+    assert.deepEqual(suggestions.strings, expected);
+
+    // If user types "pm-with" operator, an email and a comma,
+    // show suggestions for group direct messages with the "dm"
+    // operator.
+    query = "pm-with:bob@zulip.com,";
+    suggestions = get_suggestions("", query);
+    expected = [
+        "pm-with:bob@zulip.com,",
+        "dm:bob@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com",
+    ];
     assert.deepEqual(suggestions.strings, expected);
 
     // Test multiple operators
-    query = "is:starred has:link pm-with:bob@zulip.com,Smit";
+    query = "is:starred has:link dm:bob@zulip.com,Smit";
     suggestions = get_suggestions("", query);
     expected = [
-        "is:starred has:link pm-with:bob@zulip.com,Smit",
-        "is:starred has:link pm-with:bob@zulip.com,ted@zulip.com",
+        "is:starred has:link dm:bob@zulip.com,Smit",
+        "is:starred has:link dm:bob@zulip.com,ted@zulip.com",
         "is:starred has:link",
         "is:starred",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "stream:Denmark has:link pm-with:bob@zulip.com,Smit";
+    query = "stream:Denmark has:link dm:bob@zulip.com,Smit";
     suggestions = get_suggestions("", query);
     expected = [
-        "stream:Denmark has:link pm-with:bob@zulip.com,Smit",
+        "stream:Denmark has:link dm:bob@zulip.com,Smit",
         "stream:Denmark has:link",
         "stream:Denmark",
     ];
@@ -324,42 +363,45 @@ test("group_suggestions", () => {
         message([bob.user_id, ted.user_id, jeff.user_id], 98),
     ]);
 
-    // Simulate a past huddle which should now prioritize ted over alice
-    query = "pm-with:bob@zulip.com,";
+    // Simulate a past group direct message which should now
+    // prioritize ted over alice
+    query = "dm:bob@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "pm-with:bob@zulip.com,",
-        "pm-with:bob@zulip.com,ted@zulip.com",
-        "pm-with:bob@zulip.com,alice@zulip.com",
-        "pm-with:bob@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,",
+        "dm:bob@zulip.com,ted@zulip.com",
+        "dm:bob@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,jeff@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    // bob,ted,jeff is already an existing huddle, so prioritize this one
-    query = "pm-with:bob@zulip.com,ted@zulip.com,";
+    // bob, ted, and jeff are already an existing group direct message,
+    // so prioritize this one
+    query = "dm:bob@zulip.com,ted@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "pm-with:bob@zulip.com,ted@zulip.com,",
-        "pm-with:bob@zulip.com,ted@zulip.com,jeff@zulip.com",
-        "pm-with:bob@zulip.com,ted@zulip.com,alice@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com,",
+        "dm:bob@zulip.com,ted@zulip.com,jeff@zulip.com",
+        "dm:bob@zulip.com,ted@zulip.com,alice@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    // bob,ted,jeff is already an existing huddle, but if we start with just jeff,
-    // then don't prioritize ted over alice because it doesn't complete the full huddle.
-    query = "pm-with:jeff@zulip.com,";
+    // bob, ted, and jeff are already an existing group direct message,
+    // but if we start with just jeff, then don't prioritize ted over
+    // alice because it doesn't complete the full group direct message.
+    query = "dm:jeff@zulip.com,";
     suggestions = get_suggestions("", query);
     expected = [
-        "pm-with:jeff@zulip.com,",
-        "pm-with:jeff@zulip.com,alice@zulip.com",
-        "pm-with:jeff@zulip.com,bob@zulip.com",
-        "pm-with:jeff@zulip.com,ted@zulip.com",
+        "dm:jeff@zulip.com,",
+        "dm:jeff@zulip.com,alice@zulip.com",
+        "dm:jeff@zulip.com,bob@zulip.com",
+        "dm:jeff@zulip.com,ted@zulip.com",
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "pm-with:jeff@zulip.com,ted@zulip.com hi";
+    query = "dm:jeff@zulip.com,ted@zulip.com hi";
     suggestions = get_suggestions("", query);
-    expected = ["pm-with:jeff@zulip.com,ted@zulip.com hi", "pm-with:jeff@zulip.com,ted@zulip.com"];
+    expected = ["dm:jeff@zulip.com,ted@zulip.com hi", "dm:jeff@zulip.com,ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 });
 
@@ -374,7 +416,7 @@ test("empty_query_suggestions", () => {
     const expected = [
         "",
         "streams:public",
-        "is:private",
+        "is:dm",
         "is:starred",
         "is:mentioned",
         "is:alerted",
@@ -393,7 +435,7 @@ test("empty_query_suggestions", () => {
     function describe(q) {
         return suggestions.lookup_table.get(q).description_html;
     }
-    assert.equal(describe("is:private"), "Direct messages");
+    assert.equal(describe("is:dm"), "Direct messages");
     assert.equal(describe("is:starred"), "Starred messages");
     assert.equal(describe("is:mentioned"), "@-mentions");
     assert.equal(describe("is:alerted"), "Alerted messages");
@@ -474,15 +516,15 @@ test("check_is_suggestions", ({override}) => {
     let suggestions = get_suggestions("", query);
     let expected = [
         "i",
-        "is:private",
+        "is:dm",
         "is:starred",
         "is:mentioned",
         "is:alerted",
         "is:unread",
         "is:resolved",
         "sender:alice@zulip.com",
-        "pm-with:alice@zulip.com",
-        "group-pm-with:alice@zulip.com",
+        "dm:alice@zulip.com",
+        "dm-including:alice@zulip.com",
         "has:image",
     ];
     assert.deepEqual(suggestions.strings, expected);
@@ -491,7 +533,7 @@ test("check_is_suggestions", ({override}) => {
         return suggestions.lookup_table.get(q).description_html;
     }
 
-    assert.equal(describe("is:private"), "Direct messages");
+    assert.equal(describe("is:dm"), "Direct messages");
     assert.equal(describe("is:starred"), "Starred messages");
     assert.equal(describe("is:mentioned"), "@-mentions");
     assert.equal(describe("is:alerted"), "Alerted messages");
@@ -502,7 +544,7 @@ test("check_is_suggestions", ({override}) => {
     suggestions = get_suggestions("", query);
     expected = [
         "-i",
-        "-is:private",
+        "-is:dm",
         "-is:starred",
         "-is:mentioned",
         "-is:alerted",
@@ -511,7 +553,7 @@ test("check_is_suggestions", ({override}) => {
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    assert.equal(describe("-is:private"), "Exclude direct messages");
+    assert.equal(describe("-is:dm"), "Exclude direct messages");
     assert.equal(describe("-is:starred"), "Exclude starred messages");
     assert.equal(describe("-is:mentioned"), "Exclude @-mentions");
     assert.equal(describe("-is:alerted"), "Exclude alerted messages");
@@ -522,14 +564,7 @@ test("check_is_suggestions", ({override}) => {
 
     query = "is:";
     suggestions = get_suggestions("", query);
-    expected = [
-        "is:private",
-        "is:starred",
-        "is:mentioned",
-        "is:alerted",
-        "is:unread",
-        "is:resolved",
-    ];
+    expected = ["is:dm", "is:starred", "is:mentioned", "is:alerted", "is:unread", "is:resolved"];
     assert.deepEqual(suggestions.strings, expected);
 
     query = "is:st";
@@ -639,12 +674,7 @@ test("topic_suggestions", ({override}) => {
     stream_data.add_sub({stream_id: office_id, name: "office", subscribed: true});
 
     suggestions = get_suggestions("", "te");
-    expected = [
-        "te",
-        "sender:ted@zulip.com",
-        "pm-with:ted@zulip.com",
-        "group-pm-with:ted@zulip.com",
-    ];
+    expected = ["te", "sender:ted@zulip.com", "dm:ted@zulip.com", "dm-including:ted@zulip.com"];
     assert.deepEqual(suggestions.strings, expected);
 
     stream_topic_history.add_message({
@@ -663,8 +693,8 @@ test("topic_suggestions", ({override}) => {
     expected = [
         "te",
         "sender:ted@zulip.com",
-        "pm-with:ted@zulip.com",
-        "group-pm-with:ted@zulip.com",
+        "dm:ted@zulip.com",
+        "dm-including:ted@zulip.com",
         "stream:office topic:team",
         "stream:office topic:test",
     ];
@@ -702,8 +732,8 @@ test("topic_suggestions", ({override}) => {
     ];
     assert.deepEqual(suggestions.strings, expected);
 
-    suggestions = get_suggestions("", "is:private stream:devel topic:");
-    expected = ["is:private stream:devel topic:", "is:private stream:devel", "is:private"];
+    suggestions = get_suggestions("", "is:dm stream:devel topic:");
+    expected = ["is:dm stream:devel topic:", "is:dm stream:devel", "is:dm"];
     assert.deepEqual(suggestions.strings, expected);
 
     suggestions = get_suggestions("", "topic:REXX stream:devel topic:");
@@ -822,10 +852,10 @@ test("people_suggestions", ({override}) => {
         "te",
         "sender:bob@zulip.com",
         "sender:ted@zulip.com",
-        "pm-with:bob@zulip.com", // bob térry
-        "pm-with:ted@zulip.com",
-        "group-pm-with:bob@zulip.com",
-        "group-pm-with:ted@zulip.com",
+        "dm:bob@zulip.com", // bob térry
+        "dm:ted@zulip.com",
+        "dm-including:bob@zulip.com",
+        "dm-including:ted@zulip.com",
     ];
 
     assert.deepEqual(suggestions.strings, expected);
@@ -833,23 +863,23 @@ test("people_suggestions", ({override}) => {
     function is_person(q) {
         return suggestions.lookup_table.get(q).is_person;
     }
-    assert.equal(is_person("pm-with:ted@zulip.com"), true);
+    assert.equal(is_person("dm:ted@zulip.com"), true);
     assert.equal(is_person("sender:ted@zulip.com"), true);
-    assert.equal(is_person("group-pm-with:ted@zulip.com"), true);
+    assert.equal(is_person("dm-including:ted@zulip.com"), true);
 
     function has_image(q) {
         return suggestions.lookup_table.get(q).user_pill_context.has_image;
     }
-    assert.equal(has_image("pm-with:bob@zulip.com"), true);
+    assert.equal(has_image("dm:bob@zulip.com"), true);
     assert.equal(has_image("sender:bob@zulip.com"), true);
-    assert.equal(has_image("group-pm-with:bob@zulip.com"), true);
+    assert.equal(has_image("dm-including:bob@zulip.com"), true);
 
     function describe(q) {
         return suggestions.lookup_table.get(q).description_html;
     }
-    assert.equal(describe("pm-with:ted@zulip.com"), "Direct messages with");
+    assert.equal(describe("dm:ted@zulip.com"), "Direct messages with");
     assert.equal(describe("sender:ted@zulip.com"), "Sent by");
-    assert.equal(describe("group-pm-with:ted@zulip.com"), "Group direct messages including");
+    assert.equal(describe("dm-including:ted@zulip.com"), "Direct messages including");
 
     let expectedString = "<strong>Te</strong>d Smith";
 
@@ -857,26 +887,21 @@ test("people_suggestions", ({override}) => {
         return suggestions.lookup_table.get(q).user_pill_context.display_value.string;
     }
     assert.equal(get_full_name("sender:ted@zulip.com"), expectedString);
-    assert.equal(get_full_name("pm-with:ted@zulip.com"), expectedString);
-    assert.equal(get_full_name("group-pm-with:ted@zulip.com"), expectedString);
+    assert.equal(get_full_name("dm:ted@zulip.com"), expectedString);
+    assert.equal(get_full_name("dm-including:ted@zulip.com"), expectedString);
 
     expectedString = example_avatar_url + "?s=50";
 
     function get_avatar_url(q) {
         return suggestions.lookup_table.get(q).user_pill_context.img_src;
     }
-    assert.equal(get_avatar_url("pm-with:bob@zulip.com"), expectedString);
+    assert.equal(get_avatar_url("dm:bob@zulip.com"), expectedString);
     assert.equal(get_avatar_url("sender:bob@zulip.com"), expectedString);
-    assert.equal(get_avatar_url("group-pm-with:bob@zulip.com"), expectedString);
+    assert.equal(get_avatar_url("dm-including:bob@zulip.com"), expectedString);
 
     suggestions = get_suggestions("", "Ted "); // note space
 
-    expected = [
-        "Ted",
-        "sender:ted@zulip.com",
-        "pm-with:ted@zulip.com",
-        "group-pm-with:ted@zulip.com",
-    ];
+    expected = ["Ted", "sender:ted@zulip.com", "dm:ted@zulip.com", "dm-including:ted@zulip.com"];
 
     assert.deepEqual(suggestions.strings, expected);
 
@@ -910,11 +935,6 @@ test("operator_suggestions", ({override}) => {
     expected = ["st", "streams:public", "is:starred", "stream:"];
     assert.deepEqual(suggestions.strings, expected);
 
-    query = "group-";
-    suggestions = get_suggestions("", query);
-    expected = ["group-", "group-pm-with:"];
-    assert.deepEqual(suggestions.strings, expected);
-
     query = "-s";
     suggestions = get_suggestions("", query);
     expected = ["-s", "-streams:public", "-sender:myself@zulip.com", "-stream:", "-sender:"];
diff --git a/web/tests/typing_events.test.js b/web/tests/typing_events.test.js
--- a/web/tests/typing_events.test.js
+++ b/web/tests/typing_events.test.js
@@ -46,7 +46,7 @@ run_test("render_notifications_for_narrow", ({override, mock_template}) => {
     override(page_params, "user_id", anna.user_id);
     const group = [anna.user_id, vronsky.user_id, levin.user_id, kitty.user_id];
     const group_emails = `${anna.email},${vronsky.email},${levin.email},${kitty.email}`;
-    narrow_state.set_current_filter(new Filter([{operator: "pm-with", operand: group_emails}]));
+    narrow_state.set_current_filter(new Filter([{operator: "dm", operand: group_emails}]));
 
     const $typing_notifications = $("#typing_notifications");
 
diff --git a/zerver/tests/fixtures/narrow.json b/zerver/tests/fixtures/narrow.json
--- a/zerver/tests/fixtures/narrow.json
+++ b/zerver/tests/fixtures/narrow.json
@@ -146,6 +146,30 @@
             ]
         ]
     },
+    {
+        "accept_events":[
+            {
+                "message":{
+                    "type":"private"
+                },
+                "flags":null
+            }
+        ],
+        "reject_events":[
+            {
+                "message":{
+                    "type":"stream"
+                },
+                "flags":null
+            }
+        ],
+        "narrow":[
+            [
+                "is",
+                "dm"
+            ]
+        ]
+    },
     {
         "accept_events":[
             {
diff --git a/zerver/tests/test_email_notifications.py b/zerver/tests/test_email_notifications.py
--- a/zerver/tests/test_email_notifications.py
+++ b/zerver/tests/test_email_notifications.py
@@ -1449,7 +1449,7 @@ def test_pm_link_in_missed_message_header(self) -> None:
 
         encoded_name = "Cordelia,-Lear's-daughter"
         verify_body_include = [
-            f"view it in Zulip Dev Zulip: http://zulip.testserver/#narrow/pm-with/{cordelia.id}-{encoded_name}"
+            f"view it in Zulip Dev Zulip: http://zulip.testserver/#narrow/dm/{cordelia.id}-{encoded_name}"
         ]
         email_subject = "DMs with Cordelia, Lear's daughter"
         self._test_cases(msg_id, verify_body_include, email_subject, send_as_user=False)
diff --git a/zerver/tests/test_message_fetch.py b/zerver/tests/test_message_fetch.py
--- a/zerver/tests/test_message_fetch.py
+++ b/zerver/tests/test_message_fetch.py
@@ -127,10 +127,6 @@ def test_add_term_using_stream_operator_and_non_existing_operand_should_raise_er
         term = dict(operator="stream", operand="NonExistingStream")
         self.assertRaises(BadNarrowOperatorError, self._build_query, term)
 
-    def test_add_term_using_is_operator_and_private_operand(self) -> None:
-        term = dict(operator="is", operand="private")
-        self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) != %(param_1)s")
-
     def test_add_term_using_streams_operator_and_invalid_operand_should_raise_error(
         self,
     ) -> None:  # NEGATED
@@ -209,11 +205,15 @@ def test_add_term_using_streams_operator_and_public_stream_operand_negated(self)
             "WHERE (recipient_id NOT IN (__[POSTCOMPILE_recipient_id_1]))",
         )
 
-    def test_add_term_using_is_operator_private_operand_and_negated(self) -> None:  # NEGATED
-        term = dict(operator="is", operand="private", negated=True)
+    def test_add_term_using_is_operator_and_dm_operand(self) -> None:
+        term = dict(operator="is", operand="dm")
+        self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) != %(param_1)s")
+
+    def test_add_term_using_is_operator_dm_operand_and_negated(self) -> None:  # NEGATED
+        term = dict(operator="is", operand="dm", negated=True)
         self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) = %(param_1)s")
 
-    def test_add_term_using_is_operator_and_non_private_operand(self) -> None:
+    def test_add_term_using_is_operator_and_non_dm_operand(self) -> None:
         for operand in ["starred", "mentioned", "alerted"]:
             term = dict(operator="is", operand=operand)
             self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) != %(param_1)s")
@@ -226,7 +226,7 @@ def test_add_term_using_is_operator_and_unread_operand_and_negated(self) -> None
         term = dict(operator="is", operand="unread", negated=True)
         self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) != %(param_1)s")
 
-    def test_add_term_using_is_operator_non_private_operand_and_negated(self) -> None:  # NEGATED
+    def test_add_term_using_is_operator_non_dm_operand_and_negated(self) -> None:  # NEGATED
         term = dict(operator="is", operand="starred", negated=True)
         where_clause = "WHERE (flags & %(flags_1)s) = %(param_1)s"
         params = dict(
@@ -295,60 +295,60 @@ def test_add_term_using_sender_operator_with_non_existing_user_as_operand(
         term = dict(operator="sender", operand="non-existing@zulip.com")
         self.assertRaises(BadNarrowOperatorError, self._build_query, term)
 
-    def test_add_term_using_pm_with_operator_and_not_the_same_user_as_operand(self) -> None:
-        term = dict(operator="pm-with", operand=self.othello_email)
+    def test_add_term_using_dm_operator_and_not_the_same_user_as_operand(self) -> None:
+        term = dict(operator="dm", operand=self.othello_email)
         self._do_add_term_test(
             term,
             "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s",
         )
 
-    def test_add_term_using_pm_with_operator_not_the_same_user_as_operand_and_negated(
+    def test_add_term_using_dm_operator_not_the_same_user_as_operand_and_negated(
         self,
     ) -> None:  # NEGATED
-        term = dict(operator="pm-with", operand=self.othello_email, negated=True)
+        term = dict(operator="dm", operand=self.othello_email, negated=True)
         self._do_add_term_test(
             term,
             "WHERE NOT (sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s)",
         )
 
-    def test_add_term_using_pm_with_operator_the_same_user_as_operand(self) -> None:
-        term = dict(operator="pm-with", operand=self.hamlet_email)
+    def test_add_term_using_dm_operator_the_same_user_as_operand(self) -> None:
+        term = dict(operator="dm", operand=self.hamlet_email)
         self._do_add_term_test(
             term, "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s"
         )
 
-    def test_add_term_using_pm_with_operator_the_same_user_as_operand_and_negated(
+    def test_add_term_using_dm_operator_the_same_user_as_operand_and_negated(
         self,
     ) -> None:  # NEGATED
-        term = dict(operator="pm-with", operand=self.hamlet_email, negated=True)
+        term = dict(operator="dm", operand=self.hamlet_email, negated=True)
         self._do_add_term_test(
             term, "WHERE NOT (sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s)"
         )
 
-    def test_add_term_using_pm_with_operator_and_self_and_user_as_operand(self) -> None:
+    def test_add_term_using_dm_operator_and_self_and_user_as_operand(self) -> None:
         myself_and_other = ",".join(
             [
                 self.example_user("hamlet").email,
                 self.example_user("othello").email,
             ]
         )
-        term = dict(operator="pm-with", operand=myself_and_other)
+        term = dict(operator="dm", operand=myself_and_other)
         self._do_add_term_test(
             term,
             "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s",
         )
 
-    def test_add_term_using_pm_with_operator_more_than_one_user_as_operand(self) -> None:
+    def test_add_term_using_dm_operator_more_than_one_user_as_operand(self) -> None:
         two_others = ",".join(
             [
                 self.example_user("cordelia").email,
                 self.example_user("othello").email,
             ]
         )
-        term = dict(operator="pm-with", operand=two_others)
+        term = dict(operator="dm", operand=two_others)
         self._do_add_term_test(term, "WHERE recipient_id = %(recipient_id_1)s")
 
-    def test_add_term_using_pm_with_operator_self_and_user_as_operand_and_negated(
+    def test_add_term_using_dm_operator_self_and_user_as_operand_and_negated(
         self,
     ) -> None:  # NEGATED
         myself_and_other = ",".join(
@@ -357,34 +357,66 @@ def test_add_term_using_pm_with_operator_self_and_user_as_operand_and_negated(
                 self.example_user("othello").email,
             ]
         )
-        term = dict(operator="pm-with", operand=myself_and_other, negated=True)
+        term = dict(operator="dm", operand=myself_and_other, negated=True)
         self._do_add_term_test(
             term,
             "WHERE NOT (sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s)",
         )
 
-    def test_add_term_using_pm_with_operator_more_than_one_user_as_operand_and_negated(
+    def test_add_term_using_dm_operator_more_than_one_user_as_operand_and_negated(
         self,
-    ) -> None:
+    ) -> None:  # NEGATED
         two_others = ",".join(
             [
                 self.example_user("cordelia").email,
                 self.example_user("othello").email,
             ]
         )
-        term = dict(operator="pm-with", operand=two_others, negated=True)
+        term = dict(operator="dm", operand=two_others, negated=True)
         self._do_add_term_test(term, "WHERE recipient_id != %(recipient_id_1)s")
 
-    def test_add_term_using_pm_with_operator_with_comma_noise(self) -> None:
-        term = dict(operator="pm-with", operand=" ,,, ,,, ,")
+    def test_add_term_using_dm_operator_with_comma_noise(self) -> None:
+        term = dict(operator="dm", operand=" ,,, ,,, ,")
         self.assertRaises(BadNarrowOperatorError, self._build_query, term)
 
-    def test_add_term_using_pm_with_operator_with_existing_and_non_existing_user_as_operand(
+    def test_add_term_using_dm_operator_with_existing_and_non_existing_user_as_operand(
         self,
     ) -> None:
-        term = dict(operator="pm-with", operand=self.othello_email + ",non-existing@zulip.com")
+        term = dict(operator="dm", operand=self.othello_email + ",non-existing@zulip.com")
         self.assertRaises(BadNarrowOperatorError, self._build_query, term)
 
+    def test_add_term_using_dm_including_operator_with_logged_in_user_email(self) -> None:
+        term = dict(operator="dm-including", operand=self.hamlet_email)
+        self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) != %(param_1)s")
+
+    def test_add_term_using_dm_including_operator_with_different_user_email(self) -> None:
+        # Test without any such group direct messages existing
+        term = dict(operator="dm-including", operand=self.othello_email)
+        self._do_add_term_test(
+            term,
+            "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s OR recipient_id IN (__[POSTCOMPILE_recipient_id_3])",
+        )
+
+        # Test with at least one such group direct messages existing
+        self.send_huddle_message(
+            self.user_profile, [self.example_user("othello"), self.example_user("cordelia")]
+        )
+
+        term = dict(operator="dm-including", operand=self.othello_email)
+        self._do_add_term_test(
+            term,
+            "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s OR recipient_id IN (__[POSTCOMPILE_recipient_id_3])",
+        )
+
+    def test_add_term_using_dm_including_operator_with_different_user_email_and_negated(
+        self,
+    ) -> None:  # NEGATED
+        term = dict(operator="dm-including", operand=self.othello_email, negated=True)
+        self._do_add_term_test(
+            term,
+            "WHERE NOT (sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s OR sender_id = %(sender_id_2)s AND recipient_id = %(recipient_id_2)s OR recipient_id IN (__[POSTCOMPILE_recipient_id_3]))",
+        )
+
     def test_add_term_using_id_operator(self) -> None:
         term = dict(operator="id", operand=555)
         self._do_add_term_test(term, "WHERE id = %(param_1)s")
@@ -400,29 +432,6 @@ def test_add_term_using_id_operator_and_negated(self) -> None:  # NEGATED
         term = dict(operator="id", operand=555, negated=True)
         self._do_add_term_test(term, "WHERE id != %(param_1)s")
 
-    def test_add_term_using_group_pm_operator_and_not_the_same_user_as_operand(self) -> None:
-        # Test without any such group PM threads existing
-        term = dict(operator="group-pm-with", operand=self.othello_email)
-        self._do_add_term_test(term, "WHERE recipient_id IN (__[POSTCOMPILE_recipient_id_1])")
-
-        # Test with at least one such group PM thread existing
-        self.send_huddle_message(
-            self.user_profile, [self.example_user("othello"), self.example_user("cordelia")]
-        )
-
-        term = dict(operator="group-pm-with", operand=self.othello_email)
-        self._do_add_term_test(term, "WHERE recipient_id IN (__[POSTCOMPILE_recipient_id_1])")
-
-    def test_add_term_using_group_pm_operator_not_the_same_user_as_operand_and_negated(
-        self,
-    ) -> None:  # NEGATED
-        term = dict(operator="group-pm-with", operand=self.othello_email, negated=True)
-        self._do_add_term_test(term, "WHERE (recipient_id NOT IN (__[POSTCOMPILE_recipient_id_1]))")
-
-    def test_add_term_using_group_pm_operator_with_non_existing_user_as_operand(self) -> None:
-        term = dict(operator="group-pm-with", operand="non-existing@zulip.com")
-        self.assertRaises(BadNarrowOperatorError, self._build_query, term)
-
     @override_settings(USING_PGROONGA=False)
     def test_add_term_using_search_operator(self) -> None:
         term = dict(operator="search", operand='"french fries"')
@@ -522,6 +531,22 @@ def _build_query(term: Dict[str, Any]) -> Select:
 
         self.assertRaises(BadNarrowOperatorError, _build_query, term)
 
+    # Test "is:private" (legacy alias for "is:dm")
+    def test_add_term_using_is_operator_and_private_operand(self) -> None:
+        term = dict(operator="is", operand="private")
+        self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) != %(param_1)s")
+
+    def test_add_term_using_is_operator_private_operand_and_negated(self) -> None:  # NEGATED
+        term = dict(operator="is", operand="private", negated=True)
+        self._do_add_term_test(term, "WHERE (flags & %(flags_1)s) = %(param_1)s")
+
+    # Test that "pm-with" (legacy alias for "dm") works.
+    def test_add_term_using_pm_with_operator(self) -> None:
+        term = dict(operator="pm-with", operand=self.hamlet_email)
+        self._do_add_term_test(
+            term, "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s"
+        )
+
     # Test that the underscore version of "pm-with" works.
     def test_add_term_using_underscore_version_of_pm_with_operator(self) -> None:
         term = dict(operator="pm_with", operand=self.hamlet_email)
@@ -529,6 +554,25 @@ def test_add_term_using_underscore_version_of_pm_with_operator(self) -> None:
             term, "WHERE sender_id = %(sender_id_1)s AND recipient_id = %(recipient_id_1)s"
         )
 
+    # Test that deprecated "group-pm-with" (replaced by "dm-including" ) works.
+    def test_add_term_using_dm_including_operator_with_non_existing_user(self) -> None:
+        term = dict(operator="dm-including", operand="non-existing@zulip.com")
+        self.assertRaises(BadNarrowOperatorError, self._build_query, term)
+
+    def test_add_term_using_group_pm_operator_and_not_the_same_user_as_operand(self) -> None:
+        term = dict(operator="group-pm-with", operand=self.othello_email)
+        self._do_add_term_test(term, "WHERE recipient_id IN (__[POSTCOMPILE_recipient_id_1])")
+
+    def test_add_term_using_group_pm_operator_not_the_same_user_as_operand_and_negated(
+        self,
+    ) -> None:  # NEGATED
+        term = dict(operator="group-pm-with", operand=self.othello_email, negated=True)
+        self._do_add_term_test(term, "WHERE (recipient_id NOT IN (__[POSTCOMPILE_recipient_id_1]))")
+
+    def test_add_term_using_group_pm_operator_with_non_existing_user_as_operand(self) -> None:
+        term = dict(operator="group-pm-with", operand="non-existing@zulip.com")
+        self.assertRaises(BadNarrowOperatorError, self._build_query, term)
+
     # Test that the underscore version of "group-pm-with" works.
     def test_add_term_using_underscore_version_of_group_pm_with_operator(self) -> None:
         term = dict(operator="group_pm_with", operand=self.othello_email)
@@ -552,7 +596,7 @@ def test_build_narrow_filter(self) -> None:
         fixtures_path = os.path.join(os.path.dirname(__file__), "fixtures/narrow.json")
         with open(fixtures_path, "rb") as f:
             scenarios = orjson.loads(f.read())
-        self.assert_length(scenarios, 10)
+        self.assert_length(scenarios, 11)
         for scenario in scenarios:
             narrow = scenario["narrow"]
             accept_events = scenario["accept_events"]
@@ -582,10 +626,10 @@ def test_is_spectator_compatible(self) -> None:
             is_spectator_compatible([{"operator": "sender", "operand": "hamlet@zulip.com"}])
         )
         self.assertFalse(
-            is_spectator_compatible([{"operator": "pm-with", "operand": "hamlet@zulip.com"}])
+            is_spectator_compatible([{"operator": "dm", "operand": "hamlet@zulip.com"}])
         )
         self.assertFalse(
-            is_spectator_compatible([{"operator": "group-pm-with", "operand": "hamlet@zulip.com"}])
+            is_spectator_compatible([{"operator": "dm-including", "operand": "hamlet@zulip.com"}])
         )
         self.assertTrue(is_spectator_compatible([{"operator": "stream", "operand": "Denmark"}]))
         self.assertTrue(
@@ -597,11 +641,23 @@ def test_is_spectator_compatible(self) -> None:
             )
         )
         self.assertFalse(is_spectator_compatible([{"operator": "is", "operand": "starred"}]))
-        self.assertFalse(is_spectator_compatible([{"operator": "is", "operand": "private"}]))
+        self.assertFalse(is_spectator_compatible([{"operator": "is", "operand": "dm"}]))
         self.assertTrue(is_spectator_compatible([{"operator": "streams", "operand": "public"}]))
+
         # Malformed input not allowed
         self.assertFalse(is_spectator_compatible([{"operator": "has"}]))
 
+        # "is:private" is a legacy alias for "is:dm".
+        self.assertFalse(is_spectator_compatible([{"operator": "is", "operand": "private"}]))
+        # "pm-with:"" is a legacy alias for "dm:"
+        self.assertFalse(
+            is_spectator_compatible([{"operator": "pm-with", "operand": "hamlet@zulip.com"}])
+        )
+        # "group-pm-with:" was deprecated by the addition of "dm-including:"
+        self.assertFalse(
+            is_spectator_compatible([{"operator": "group-pm-with", "operand": "hamlet@zulip.com"}])
+        )
+
 
 class IncludeHistoryTest(ZulipTestCase):
     def test_ok_to_include_history(self) -> None:
@@ -651,7 +707,12 @@ def test_ok_to_include_history(self) -> None:
         self.assertFalse(ok_to_include_history(narrow, user_profile, False))
         self.assertTrue(ok_to_include_history(narrow, subscribed_user_profile, False))
 
-        # History doesn't apply to PMs.
+        # History doesn't apply to direct messages.
+        narrow = [
+            dict(operator="is", operand="dm"),
+        ]
+        self.assertFalse(ok_to_include_history(narrow, user_profile, False))
+        # "is:private" is a legacy alias for "is:dm".
         narrow = [
             dict(operator="is", operand="private"),
         ]
@@ -1497,7 +1558,7 @@ def test_successful_get_messages(self) -> None:
         self.get_and_check_messages(
             dict(
                 narrow=orjson.dumps(
-                    [["pm-with", othello_email]],
+                    [["dm", othello_email]],
                 ).decode(),
             ),
         )
@@ -1505,7 +1566,7 @@ def test_successful_get_messages(self) -> None:
         self.get_and_check_messages(
             dict(
                 narrow=orjson.dumps(
-                    [dict(operator="pm-with", operand=othello_email)],
+                    [dict(operator="dm", operand=othello_email)],
                 ).decode(),
             ),
         )
@@ -1539,7 +1600,14 @@ def test_unauthenticated_get_messages(self) -> None:
             result = self.client_get("/json/messages", dict(web_public_stream_get_params))
             self.assert_json_error(result, "Invalid subdomain", status_code=404)
 
-        # Cannot access private messages without login.
+        # Cannot access direct messages without login.
+        direct_messages_get_params: Dict[str, Union[int, str, bool]] = {
+            **get_params,
+            "narrow": orjson.dumps([dict(operator="is", operand="dm")]).decode(),
+        }
+        result = self.client_get("/json/messages", dict(direct_messages_get_params))
+        self.check_unauthenticated_response(result)
+        # "is:private" is a legacy alias for "is:dm".
         private_message_get_params: Dict[str, Union[int, str, bool]] = {
             **get_params,
             "narrow": orjson.dumps([dict(operator="is", operand="private")]).decode(),
@@ -1550,11 +1618,11 @@ def test_unauthenticated_get_messages(self) -> None:
         # narrow should pass conditions in `is_spectator_compatible`.
         non_spectator_compatible_narrow_get_params: Dict[str, Union[int, str, bool]] = {
             **get_params,
-            # "is:private" is not a is_spectator_compatible narrow.
+            # "is:dm" is not a is_spectator_compatible narrow.
             "narrow": orjson.dumps(
                 [
                     dict(operator="streams", operand="web-public"),
-                    dict(operator="is", operand="private"),
+                    dict(operator="is", operand="dm"),
                 ]
             ).decode(),
         }
@@ -1613,7 +1681,7 @@ def test_unauthenticated_get_messages(self) -> None:
     def setup_web_public_test(self, num_web_public_message: int = 1) -> None:
         """
         Send N+2 messages, N in a web-public stream, then one in a non-web-public stream
-        and then a private message.
+        and then a direct message.
         """
         user_profile = self.example_user("iago")
         do_set_realm_property(
@@ -1633,7 +1701,7 @@ def setup_web_public_test(self, num_web_public_message: int = 1) -> None:
             user_profile, non_web_public_stream.name, content="non-web-public message"
         )
         self.send_personal_message(
-            user_profile, self.example_user("hamlet"), content="private message"
+            user_profile, self.example_user("hamlet"), content="direct message"
         )
         self.logout()
 
@@ -1729,9 +1797,9 @@ def test_client_avatar(self) -> None:
         message = result["messages"][0]
         self.assertIn("gravatar.com", message["avatar_url"])
 
-    def test_get_messages_with_narrow_pm_with(self) -> None:
+    def test_get_messages_with_narrow_dm(self) -> None:
         """
-        A request for old messages with a narrow by pm-with only returns
+        A request for old messages with a narrow by direct message only returns
         conversations with that user.
         """
         me = self.example_user("hamlet")
@@ -1751,8 +1819,8 @@ def dr_ids(dr: DisplayRecipientT) -> List[int]:
             [self.example_user("iago"), self.example_user("cordelia")],
         )
 
-        # Send a 1:1 and group PM containing Aaron.
-        # Then deactivate aaron to test pm-with narrow includes messages
+        # Send a 1:1 and group direct message containing Aaron.
+        # Then deactivate Aaron to test "dm" narrow includes messages
         # from deactivated users also.
         self.send_personal_message(me, self.example_user("aaron"))
         self.send_huddle_message(
@@ -1769,21 +1837,21 @@ def dr_ids(dr: DisplayRecipientT) -> List[int]:
         for personal in personals:
             emails = dr_emails(get_display_recipient(personal.recipient))
             self.login_user(me)
-            narrow: List[Dict[str, Any]] = [dict(operator="pm-with", operand=emails)]
+            narrow: List[Dict[str, Any]] = [dict(operator="dm", operand=emails)]
             result = self.get_and_check_messages(dict(narrow=orjson.dumps(narrow).decode()))
 
             for message in result["messages"]:
                 self.assertEqual(dr_emails(message["display_recipient"]), emails)
 
-            # check passing id is consistent with passing emails as operand
+            # check passing user IDs is consistent with passing user emails as operand
             ids = dr_ids(get_display_recipient(personal.recipient))
-            narrow = [dict(operator="pm-with", operand=ids)]
+            narrow = [dict(operator="dm", operand=ids)]
             result = self.get_and_check_messages(dict(narrow=orjson.dumps(narrow).decode()))
 
             for message in result["messages"]:
                 self.assertEqual(dr_emails(message["display_recipient"]), emails)
 
-    def test_get_visible_messages_with_narrow_pm_with(self) -> None:
+    def test_get_visible_messages_with_narrow_dm(self) -> None:
         me = self.example_user("hamlet")
         self.login_user(me)
         self.subscribe(self.example_user("hamlet"), "Scotland")
@@ -1792,13 +1860,125 @@ def test_get_visible_messages_with_narrow_pm_with(self) -> None:
         for i in range(5):
             message_ids.append(self.send_personal_message(me, self.example_user("iago")))
 
-        narrow = [dict(operator="pm-with", operand=self.example_user("iago").email)]
+        narrow = [dict(operator="dm", operand=self.example_user("iago").email)]
+        self.message_visibility_test(narrow, message_ids, 2)
+
+    def test_get_messages_with_narrow_dm_including(self) -> None:
+        """
+        A request for old messages with a narrow by "dm-including" only
+        returns direct messages (both group and 1:1) with that user.
+        """
+        me = self.example_user("hamlet")
+
+        iago = self.example_user("iago")
+        cordelia = self.example_user("cordelia")
+        othello = self.example_user("othello")
+
+        matching_message_ids = []
+        # group direct message, sent by current user
+        matching_message_ids.append(
+            self.send_huddle_message(
+                me,
+                [iago, cordelia, othello],
+            ),
+        )
+        # group direct message, sent by searched user
+        matching_message_ids.append(
+            self.send_huddle_message(
+                cordelia,
+                [me, othello],
+            ),
+        )
+        # group direct message, sent by another user
+        matching_message_ids.append(
+            self.send_huddle_message(
+                othello,
+                [me, cordelia],
+            ),
+        )
+        # direct 1:1 message, sent by current user to searched user
+        matching_message_ids.append(
+            self.send_personal_message(me, cordelia),
+        )
+        # direct 1:1 message, sent by searched user to current user
+        matching_message_ids.append(
+            self.send_personal_message(cordelia, me),
+        )
+
+        non_matching_message_ids = []
+        # direct 1:1 message, does not include current user
+        non_matching_message_ids.append(
+            self.send_personal_message(iago, cordelia),
+        )
+        # direct 1:1 message, does not include searched user
+        non_matching_message_ids.append(
+            self.send_personal_message(iago, me),
+        )
+        # direct 1:1 message, current user to self
+        non_matching_message_ids.append(
+            self.send_personal_message(me, me),
+        )
+        # group direct message, sent by current user
+        non_matching_message_ids.append(
+            self.send_huddle_message(
+                me,
+                [iago, othello],
+            ),
+        )
+        # group direct message, sent by searched user
+        non_matching_message_ids.append(
+            self.send_huddle_message(
+                cordelia,
+                [iago, othello],
+            ),
+        )
+
+        self.login_user(me)
+        test_operands = [cordelia.email, cordelia.id]
+        for operand in test_operands:
+            narrow = [dict(operator="dm-including", operand=operand)]
+            result = self.get_and_check_messages(dict(narrow=orjson.dumps(narrow).decode()))
+            for message in result["messages"]:
+                self.assertIn(message["id"], matching_message_ids)
+                self.assertNotIn(message["id"], non_matching_message_ids)
+
+    def test_get_visible_messages_with_narrow_dm_including(self) -> None:
+        me = self.example_user("hamlet")
+        self.login_user(me)
+
+        iago = self.example_user("iago")
+        cordelia = self.example_user("cordelia")
+        othello = self.example_user("othello")
+
+        message_ids = []
+        message_ids.append(
+            self.send_huddle_message(
+                me,
+                [iago, cordelia, othello],
+            ),
+        )
+        message_ids.append(self.send_personal_message(me, cordelia))
+        message_ids.append(
+            self.send_huddle_message(
+                cordelia,
+                [me, othello],
+            ),
+        )
+        message_ids.append(self.send_personal_message(cordelia, me))
+        message_ids.append(
+            self.send_huddle_message(
+                iago,
+                [cordelia, me],
+            ),
+        )
+
+        narrow = [dict(operator="dm-including", operand=cordelia.email)]
         self.message_visibility_test(narrow, message_ids, 2)
 
     def test_get_messages_with_narrow_group_pm_with(self) -> None:
         """
-        A request for old messages with a narrow by group-pm-with only returns
-        group-private conversations with that user.
+        A request for old messages with a narrow by deprecated "group-pm-with"
+        only returns direct message group conversations with that user.
         """
         me = self.example_user("hamlet")
 
@@ -3001,20 +3181,22 @@ def test_bad_narrow_operator(self) -> None:
     def test_invalid_narrow_operand_in_dict(self) -> None:
         self.login("hamlet")
 
-        # str or int is required for sender, group-pm-with, stream
+        # str or int is required for "sender", "stream", "dm-including" and "group-pm-with" operators
         invalid_operands = [["1"], [2], None]
         error_msg = 'elem["operand"] is not a string or integer'
-        for operand in ["sender", "group-pm-with", "stream"]:
+        for operand in ["sender", "group-pm-with", "stream", "dm-including"]:
             self.exercise_bad_narrow_operand_using_dict_api(operand, invalid_operands, error_msg)
 
-        # str or int list is required for pm-with operator
+        # str or int list is required for "dm" and "pm-with" operator
         invalid_operands = [None]
         error_msg = 'elem["operand"] is not a string or an integer list'
-        self.exercise_bad_narrow_operand_using_dict_api("pm-with", invalid_operands, error_msg)
+        for operand in ["dm", "pm-with"]:
+            self.exercise_bad_narrow_operand_using_dict_api(operand, invalid_operands, error_msg)
 
         invalid_operands = [["2"]]
         error_msg = 'elem["operand"][0] is not an integer'
-        self.exercise_bad_narrow_operand_using_dict_api("pm-with", invalid_operands, error_msg)
+        for operand in ["dm", "pm-with"]:
+            self.exercise_bad_narrow_operand_using_dict_api(operand, invalid_operands, error_msg)
 
         # For others only str is acceptable
         invalid_operands = [2, None, [1]]
@@ -3057,12 +3239,12 @@ def test_bad_narrow_stream_content(self) -> None:
 
     def test_bad_narrow_one_on_one_email_content(self) -> None:
         """
-        If an invalid 'pm-with' is requested in get_messages, an
-        error is returned.
+        If an invalid "dm" narrow is requested in get_messages,
+        an error is returned.
         """
         self.login("hamlet")
         bad_stream_content: Tuple[int, List[None], List[str]] = (0, [], ["x", "y"])
-        self.exercise_bad_narrow_operand("pm-with", bad_stream_content, "Bad value for 'narrow'")
+        self.exercise_bad_narrow_operand("dm", bad_stream_content, "Bad value for 'narrow'")
 
     def test_bad_narrow_nonexistent_stream(self) -> None:
         self.login("hamlet")
@@ -3078,14 +3260,12 @@ def test_bad_narrow_nonexistent_stream(self) -> None:
     def test_bad_narrow_nonexistent_email(self) -> None:
         self.login("hamlet")
         self.exercise_bad_narrow_operand(
-            "pm-with", ["non-existent-user@zulip.com"], "Invalid narrow operator: unknown user"
+            "dm", ["non-existent-user@zulip.com"], "Invalid narrow operator: unknown user"
         )
 
-    def test_bad_narrow_pm_with_id_list(self) -> None:
+    def test_bad_narrow_dm_id_list(self) -> None:
         self.login("hamlet")
-        self.exercise_bad_narrow_operand(
-            "pm-with", [-24], "Bad value for 'narrow': [[\"pm-with\",-24]]"
-        )
+        self.exercise_bad_narrow_operand("dm", [-24], "Bad value for 'narrow': [[\"dm\",-24]]")
 
     def test_message_without_rendered_content(self) -> None:
         """Older messages may not have rendered_content in the database"""
@@ -3564,7 +3744,7 @@ def test_get_messages_with_narrow_queries(self) -> None:
                 "anchor": 0,
                 "num_before": 0,
                 "num_after": 0,
-                "narrow": f'[["pm-with", "{othello_email}"]]',
+                "narrow": f'[["dm", "{othello_email}"]]',
             },
             sql,
         )
@@ -3576,7 +3756,7 @@ def test_get_messages_with_narrow_queries(self) -> None:
                 "anchor": 0,
                 "num_before": 1,
                 "num_after": 0,
-                "narrow": f'[["pm-with", "{othello_email}"]]',
+                "narrow": f'[["dm", "{othello_email}"]]',
             },
             sql,
         )
@@ -3588,7 +3768,7 @@ def test_get_messages_with_narrow_queries(self) -> None:
                 "anchor": 0,
                 "num_before": 0,
                 "num_after": 9,
-                "narrow": f'[["pm-with", "{othello_email}"]]',
+                "narrow": f'[["dm", "{othello_email}"]]',
             },
             sql,
         )
@@ -3654,7 +3834,7 @@ def test_get_messages_with_narrow_queries(self) -> None:
             sql,
         )
 
-        # Narrow to pms with yourself
+        # Narrow to direct messages with yourself
         sql_template = "SELECT anon_1.message_id, anon_1.flags \nFROM (SELECT message_id, flags \nFROM zerver_usermessage JOIN zerver_message ON zerver_usermessage.message_id = zerver_message.id \nWHERE user_profile_id = {hamlet_id} AND sender_id = {hamlet_id} AND recipient_id = {hamlet_recipient} ORDER BY message_id ASC \n LIMIT 10) AS anon_1 ORDER BY message_id ASC"
         sql = sql_template.format(**query_ids)
         self.common_check_get_messages_query(
@@ -3662,7 +3842,7 @@ def test_get_messages_with_narrow_queries(self) -> None:
                 "anchor": 0,
                 "num_before": 0,
                 "num_after": 9,
-                "narrow": f'[["pm-with", "{hamlet_email}"]]',
+                "narrow": f'[["dm", "{hamlet_email}"]]',
             },
             sql,
         )
@@ -4090,4 +4270,4 @@ def test_near_pm_message_url(self) -> None:
             realm=realm,
             message=message,
         )
-        self.assertEqual(url, "http://zulip.testserver/#narrow/pm-with/77,80-pm/near/555")
+        self.assertEqual(url, "http://zulip.testserver/#narrow/dm/77,80-pm/near/555")