mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	reactions: Add support for removing emoji reactions.
This commit adds support for removing reactions via DELETE requests to the /reactions endpoint with parameters emoji_name and message_id. The reaction is deleted from the database and a reaction event is sent out with 'op' set to 'remove'. Tests are added to check: 1. Removing a reaction that does not exist fails 2. When removing a reaction, the event payload and users are correct
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							8c0c12c1d9
						
					
				
				
					commit
					4491ea8d6b
				
			@@ -931,6 +931,31 @@ def do_add_reaction(user_profile, message, emoji_name):
 | 
			
		||||
    ums = UserMessage.objects.filter(message=message.id)
 | 
			
		||||
    send_event(event, [um.user_profile.id for um in ums])
 | 
			
		||||
 | 
			
		||||
def do_remove_reaction(user_profile, message, emoji_name):
 | 
			
		||||
    # type: (UserProfile, Message, text_type) -> None
 | 
			
		||||
    Reaction.objects.filter(user_profile=user_profile,
 | 
			
		||||
                            message=message,
 | 
			
		||||
                            emoji_name=emoji_name).delete()
 | 
			
		||||
 | 
			
		||||
    user_dict = {'user_id': user_profile.id,
 | 
			
		||||
                 'email': user_profile.email,
 | 
			
		||||
                 'full_name': user_profile.full_name} # type: Dict[str, Any]
 | 
			
		||||
 | 
			
		||||
    event = {'type': 'reaction',
 | 
			
		||||
             'op': 'remove',
 | 
			
		||||
             'user': user_dict,
 | 
			
		||||
             'message_id': message.id,
 | 
			
		||||
             'emoji_name': emoji_name} # type: Dict[str, Any]
 | 
			
		||||
 | 
			
		||||
    # Recipients for message update events, including reactions, are
 | 
			
		||||
    # everyone who got the original message.  This means reactions
 | 
			
		||||
    # won't live-update in preview narrows, but it's the right
 | 
			
		||||
    # performance tradeoff, since otherwise we'd need to send all
 | 
			
		||||
    # reactions to public stream messages to every browser for every
 | 
			
		||||
    # client in the organization, which doesn't scale.
 | 
			
		||||
    ums = UserMessage.objects.filter(message=message.id)
 | 
			
		||||
    send_event(event, [um.user_profile.id for um in ums])
 | 
			
		||||
 | 
			
		||||
def do_send_typing_notification(notification):
 | 
			
		||||
    # type: (Dict[str, Any]) -> None
 | 
			
		||||
    recipient_user_profiles = get_recipient_user_profiles(notification['recipient'],
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,7 @@ class ReactionMessageIDTest(ZulipTestCase):
 | 
			
		||||
        self.assert_json_error(result, "Invalid message(s)")
 | 
			
		||||
 | 
			
		||||
class ReactionTest(ZulipTestCase):
 | 
			
		||||
    def test_existing_reaction(self):
 | 
			
		||||
    def test_add_existing_reaction(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        """
 | 
			
		||||
        Creating the same reaction twice fails
 | 
			
		||||
@@ -141,8 +141,40 @@ class ReactionTest(ZulipTestCase):
 | 
			
		||||
                                  **self.api_auth(reaction_sender))
 | 
			
		||||
        self.assert_json_error(second, "Reaction already exists")
 | 
			
		||||
 | 
			
		||||
    def test_remove_nonexisting_reaction(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        """
 | 
			
		||||
        Removing a reaction twice fails
 | 
			
		||||
        """
 | 
			
		||||
        pm_sender = 'hamlet@zulip.com'
 | 
			
		||||
        pm_recipient = 'othello@zulip.com'
 | 
			
		||||
        reaction_sender = pm_recipient
 | 
			
		||||
 | 
			
		||||
        pm = self.client_post("/api/v1/messages", {"type": "private",
 | 
			
		||||
                                                   "content": "Test message",
 | 
			
		||||
                                                   "to": pm_recipient},
 | 
			
		||||
                              **self.api_auth(pm_sender))
 | 
			
		||||
        self.assert_json_success(pm)
 | 
			
		||||
        content = ujson.loads(pm.content)
 | 
			
		||||
        pm_id = content['id']
 | 
			
		||||
        add = self.client_post('/api/v1/reactions', {'message_id': pm_id,
 | 
			
		||||
                                                     'emoji': 'smile'},
 | 
			
		||||
                               **self.api_auth(reaction_sender))
 | 
			
		||||
        self.assert_json_success(add)
 | 
			
		||||
 | 
			
		||||
        first = self.client_delete('/api/v1/reactions', {'message_id': pm_id,
 | 
			
		||||
                                                         'emoji': 'smile'},
 | 
			
		||||
                                   **self.api_auth(reaction_sender))
 | 
			
		||||
        self.assert_json_success(first)
 | 
			
		||||
 | 
			
		||||
        second = self.client_delete('/api/v1/reactions', {'message_id': pm_id,
 | 
			
		||||
                                                         'emoji': 'smile'},
 | 
			
		||||
                                    **self.api_auth(reaction_sender))
 | 
			
		||||
        self.assert_json_error(second, "Reaction does not exist")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReactionEventTest(ZulipTestCase):
 | 
			
		||||
    def test_event(self):
 | 
			
		||||
    def test_add_event(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        """
 | 
			
		||||
        Recipients of the message receive the reaction event
 | 
			
		||||
@@ -180,3 +212,47 @@ class ReactionEventTest(ZulipTestCase):
 | 
			
		||||
        self.assertEqual(event['op'], 'add')
 | 
			
		||||
        self.assertEqual(event['emoji_name'], 'smile')
 | 
			
		||||
        self.assertEqual(event['message_id'], pm_id)
 | 
			
		||||
 | 
			
		||||
    def test_remove_event(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        """
 | 
			
		||||
        Recipients of the message receive the reaction event
 | 
			
		||||
        and event contains relevant data
 | 
			
		||||
        """
 | 
			
		||||
        pm_sender = 'hamlet@zulip.com'
 | 
			
		||||
        pm_recipient = 'othello@zulip.com'
 | 
			
		||||
        reaction_sender = pm_recipient
 | 
			
		||||
 | 
			
		||||
        result = self.client_post("/api/v1/messages", {"type": "private",
 | 
			
		||||
                                                       "content": "Test message",
 | 
			
		||||
                                                       "to": pm_recipient},
 | 
			
		||||
                                  **self.api_auth(pm_sender))
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
        content = ujson.loads(result.content)
 | 
			
		||||
        pm_id = content['id']
 | 
			
		||||
 | 
			
		||||
        expected_recipient_emails = set([pm_sender, pm_recipient])
 | 
			
		||||
        expected_recipient_ids = set([get_user_profile_by_email(email).id for email in expected_recipient_emails])
 | 
			
		||||
 | 
			
		||||
        add = self.client_post('/api/v1/reactions', {'message_id': pm_id,
 | 
			
		||||
                                                     'emoji': 'smile'},
 | 
			
		||||
                               **self.api_auth(reaction_sender))
 | 
			
		||||
        self.assert_json_success(add)
 | 
			
		||||
 | 
			
		||||
        events = [] # type: List[Dict[str, Any]]
 | 
			
		||||
        with tornado_redirected_to_list(events):
 | 
			
		||||
            result = self.client_delete('/api/v1/reactions', {'message_id': pm_id,
 | 
			
		||||
                                                              'emoji': 'smile'},
 | 
			
		||||
                                        **self.api_auth(reaction_sender))
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
        self.assertEqual(len(events), 1)
 | 
			
		||||
 | 
			
		||||
        event = events[0]['event']
 | 
			
		||||
        event_user_ids = set(events[0]['users'])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_recipient_ids, event_user_ids)
 | 
			
		||||
        self.assertEqual(event['user']['email'], reaction_sender)
 | 
			
		||||
        self.assertEqual(event['type'], 'reaction')
 | 
			
		||||
        self.assertEqual(event['op'], 'remove')
 | 
			
		||||
        self.assertEqual(event['emoji_name'], 'smile')
 | 
			
		||||
        self.assertEqual(event['message_id'], pm_id)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from six import text_type
 | 
			
		||||
 | 
			
		||||
from zerver.decorator import authenticated_json_post_view,\
 | 
			
		||||
    has_request_variables, REQ, to_non_negative_int
 | 
			
		||||
from zerver.lib.actions import do_add_reaction
 | 
			
		||||
from zerver.lib.actions import do_add_reaction, do_remove_reaction
 | 
			
		||||
from zerver.lib.bugdown import emoji_list
 | 
			
		||||
from zerver.lib.message import access_message
 | 
			
		||||
from zerver.lib.request import JsonableError
 | 
			
		||||
@@ -34,3 +34,27 @@ def add_reaction_backend(request, user_profile, emoji_name=REQ('emoji'),
 | 
			
		||||
    do_add_reaction(user_profile, message, emoji_name)
 | 
			
		||||
 | 
			
		||||
    return json_success()
 | 
			
		||||
 | 
			
		||||
@has_request_variables
 | 
			
		||||
def remove_reaction_backend(request, user_profile, emoji_name=REQ('emoji'),
 | 
			
		||||
                            message_id = REQ('message_id', converter=to_non_negative_int)):
 | 
			
		||||
    # type: (HttpRequest, UserProfile, text_type, int) -> HttpResponse
 | 
			
		||||
 | 
			
		||||
    # access_message will throw a JsonableError exception if the user
 | 
			
		||||
    # cannot see the message (e.g. for messages to private streams).
 | 
			
		||||
    message = access_message(user_profile, message_id)[0]
 | 
			
		||||
 | 
			
		||||
    existing_emojis = set(message.sender.realm.get_emoji().keys()) or set(emoji_list)
 | 
			
		||||
    if emoji_name not in existing_emojis:
 | 
			
		||||
        raise JsonableError(_("Emoji '%s' does not exist" % (emoji_name,)))
 | 
			
		||||
 | 
			
		||||
    # We could probably just make this check be a try/except for the
 | 
			
		||||
    # IntegrityError from it already existing, but this is a bit cleaner.
 | 
			
		||||
    if not Reaction.objects.filter(user_profile=user_profile,
 | 
			
		||||
                                   message=message,
 | 
			
		||||
                                   emoji_name=emoji_name).exists():
 | 
			
		||||
        raise JsonableError(_("Reaction does not exist"))
 | 
			
		||||
 | 
			
		||||
    do_remove_reaction(user_profile, message, emoji_name)
 | 
			
		||||
 | 
			
		||||
    return json_success()
 | 
			
		||||
 
 | 
			
		||||
@@ -200,8 +200,10 @@ v1_api_and_json_patterns = [
 | 
			
		||||
 | 
			
		||||
    # reactions -> zerver.view.reactions
 | 
			
		||||
    # POST adds a reaction to a message
 | 
			
		||||
    # DELETE removes a reaction from a message
 | 
			
		||||
    url(r'^reactions$', rest_dispatch,
 | 
			
		||||
        {'POST': 'zerver.views.reactions.add_reaction_backend'}),
 | 
			
		||||
        {'POST': 'zerver.views.reactions.add_reaction_backend',
 | 
			
		||||
         'DELETE': 'zerver.views.reactions.remove_reaction_backend'}),
 | 
			
		||||
 | 
			
		||||
    # typing -> zerver.views.typing
 | 
			
		||||
    # POST sends a typing notification event to recipients
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user