Repository: zulip/zulip

Issue:
reactions: Preserve order of reactions
When users react to messages, they expect their reactions to be in the same order as they put them. What currently happens is that the reactions stay in the order they are placed, until the page is refreshed, at which point they are exactly reversed.

See the gif bellow for example:
![](https://user-images.githubusercontent.com/33805964/138609247-8e5e6bd6-930a-43d8-bcc1-b95a1b47ad47.gif)

The functions `set_clean_reactions(message)` in `reaction.js` and `_get_message_template(message_container)` in `message_list_view.js` are relevant.

See [# frontend > emoji reaction order](https://chat.zulip.org/#narrow/stream/6-frontend/topic/emoji.20reaction.20order).


Discussion:
[Further discussion on CZO](https://chat.zulip.org/#narrow/stream/6-frontend/topic/emoji.20reaction.20order/near/1271375):

- incremental reactions are stable, and new emojis go to end (the desired behavior)
- on page load the newest emojis go to front (wrong), and "newness" is about the last click
Hello @zulip/server-emoji members, this issue was labeled with the "area: emoji" label, so you may want to check it out!

<!-- areaLabelAddition -->


Resolving patch:
diff --git a/zerver/models.py b/zerver/models.py
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -2870,7 +2870,10 @@ def get_raw_db_rows(needed_ids: List[int]) -> List[Dict[str, Any]]:
             "user_profile_id",
             "user_profile__full_name",
         ]
-        return Reaction.objects.filter(message_id__in=needed_ids).values(*fields)
+        # The ordering is important here, as it makes it convenient
+        # for clients to display reactions in order without
+        # client-side sorting code.
+        return Reaction.objects.filter(message_id__in=needed_ids).values(*fields).order_by("id")
 
     def __str__(self) -> str:
         return f"{self.user_profile.email} / {self.message.id} / {self.emoji_name}"


Test patch:
diff --git a/zerver/tests/test_reactions.py b/zerver/tests/test_reactions.py
--- a/zerver/tests/test_reactions.py
+++ b/zerver/tests/test_reactions.py
@@ -87,29 +87,38 @@ def test_cached_reaction_data(self) -> None:
         """
         Formatted reactions data is saved in cache.
         """
-        sender = self.example_user("hamlet")
-        reaction_info = {
-            "emoji_name": "smile",
-        }
-        result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
+        senders = [self.example_user("hamlet"), self.example_user("cordelia")]
+        emojis = ["smile", "tada"]
+        expected_emoji_codes = ["1f642", "1f389"]
+
+        for sender, emoji in zip(senders, emojis):
+            reaction_info = {
+                "emoji_name": emoji,
+            }
+            result = self.api_post(sender, "/api/v1/messages/1/reactions", reaction_info)
+
+            self.assert_json_success(result)
+            self.assertEqual(200, result.status_code)
 
-        self.assert_json_success(result)
-        self.assertEqual(200, result.status_code)
         key = to_dict_cache_key_id(1)
         message = extract_message_dict(cache_get(key)[0])
 
         expected_reaction_data = [
             {
-                "emoji_name": "smile",
-                "emoji_code": "1f642",
+                "emoji_name": emoji,
+                "emoji_code": emoji_code,
                 "reaction_type": "unicode_emoji",
                 "user": {
                     "email": f"user{sender.id}@zulip.testserver",
                     "id": sender.id,
-                    "full_name": "King Hamlet",
+                    "full_name": sender.full_name,
                 },
                 "user_id": sender.id,
             }
+            # It's important that we preserve the loop order in this
+            # test, since this is our test to verify that we're
+            # returning reactions in chronological order.
+            for sender, emoji, emoji_code in zip(senders, emojis, expected_emoji_codes)
         ]
         self.assertEqual(expected_reaction_data, message["reactions"])