diff --git a/docs/changelog.md b/docs/changelog.md index 79bfc8e7e2..0b95bd603f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,7 @@ All notable changes to the Zulip server are documented in this file. ### Unreleased +- Added UI for marking all messages in a stream or topic as read. - Added new Attachment model to keep track of uploaded files. - Added caching of virtualenvs in development. - Fixed missing helper scripts for RabbitMQ Nagios plugins. diff --git a/static/js/popovers.js b/static/js/popovers.js index fd5c70aa4e..40eae17c18 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -383,6 +383,13 @@ exports.register_click_handlers = function () { e.preventDefault(); }); + $('body').on('click', '.sidebar-popover-mark-topic-read', function (e) { + var topic = $(e.currentTarget).attr('data-topic-name'); + var stream = $(e.currentTarget).attr('data-stream-name'); + popovers.hide_topic_sidebar_popover(); + unread.mark_topic_as_read(stream,topic); + e.stopPropagation(); + }); $('#stream_filters').on('click', '.stream-sidebar-arrow', function (e) { var elt = e.target; @@ -547,6 +554,13 @@ exports.register_click_handlers = function () { e.stopPropagation(); }); + $('body').on('click', '.mark_stream_as_read', function (e) { + var stream = $(e.currentTarget).parents('ul').attr('data-name'); + popovers.hide_stream_sidebar_popover(); + unread.mark_stream_as_read(stream); + e.stopPropagation(); + }); + $('body').on('click', '.open_stream_settings', function (e) { var stream = $(e.currentTarget).parents('ul').attr('data-name'); popovers.hide_stream_sidebar_popover(); diff --git a/static/js/unread.js b/static/js/unread.js index f2866dfef2..87ded74b28 100644 --- a/static/js/unread.js +++ b/static/js/unread.js @@ -272,6 +272,33 @@ exports.mark_current_list_as_read = function mark_current_list_as_read(options) exports.mark_messages_as_read(current_msg_list.all_messages(), options); }; +exports.mark_stream_as_read = function mark_stream_as_read(stream, cont) { + channel.post({ + url: '/json/messages/flags', + idempotent: true, + data: {messages: JSON.stringify([]), + all: false, + op: 'add', + flag: 'read', + stream_name: stream + }, + success: cont}); +}; + +exports.mark_topic_as_read = function mark_topic_as_read(stream, topic, cont) { + channel.post({ + url: '/json/messages/flags', + idempotent: true, + data: {messages: JSON.stringify([]), + all: false, + op: 'add', + flag: 'read', + topic_name: topic, + stream_name: stream + }, + success: cont}); +}; + return exports; }()); if (typeof module !== 'undefined') { diff --git a/static/templates/stream_sidebar_actions.handlebars b/static/templates/stream_sidebar_actions.handlebars index 69c70f1104..03d6c1b6e1 100644 --- a/static/templates/stream_sidebar_actions.handlebars +++ b/static/templates/stream_sidebar_actions.handlebars @@ -23,6 +23,12 @@ Compose a message to stream {{stream.name}} +
  • + + + Mark all messages in {{stream.name}} as read + +
  • diff --git a/static/templates/topic_sidebar_actions.handlebars b/static/templates/topic_sidebar_actions.handlebars index d665ad8ed1..027d3f983c 100644 --- a/static/templates/topic_sidebar_actions.handlebars +++ b/static/templates/topic_sidebar_actions.handlebars @@ -24,5 +24,12 @@
  • {{/if}} +
  • + + + Mark all messages in {{topic_name}} as read + +
  • + diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 22171394cd..fe82e859ad 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -2050,12 +2050,20 @@ def do_update_pointer(user_profile, pointer, update_flags=False): event = dict(type='pointer', pointer=pointer) send_event(event, [user_profile.id]) -def do_update_message_flags(user_profile, operation, flag, messages, all): +def do_update_message_flags(user_profile, operation, flag, messages, all, stream_obj, topic_name): flagattr = getattr(UserMessage.flags, flag) if all: log_statsd_event('bankruptcy') msgs = UserMessage.objects.filter(user_profile=user_profile) + elif stream_obj is not None: + recipient = get_recipient(Recipient.STREAM, stream_obj.id) + if topic_name: + msgs = UserMessage.objects.filter(message__recipient=recipient, + user_profile=user_profile, + message__subject__iexact=topic_name) + else: + msgs = UserMessage.objects.filter(message__recipient=recipient, user_profile=user_profile) else: msgs = UserMessage.objects.filter(user_profile=user_profile, message__id__in=messages) @@ -2092,9 +2100,13 @@ def do_update_message_flags(user_profile, operation, flag, messages, all): # are kind of magical; they are actually just testing the one bit. if operation == 'add': msgs = msgs.filter(flags=~flagattr) + if stream_obj: + messages = list(msgs.values_list('message__id', flat=True)) count = msgs.update(flags=F('flags').bitor(flagattr)) elif operation == 'remove': msgs = msgs.filter(flags=flagattr) + if stream_obj: + messages = list(msgs.values_list('message__id', flat=True)) count = msgs.update(flags=F('flags').bitand(~flagattr)) event = {'type': 'update_message_flags', diff --git a/zerver/management/commands/bankrupt_users.py b/zerver/management/commands/bankrupt_users.py index 9a3b99cfe4..c302d714e2 100644 --- a/zerver/management/commands/bankrupt_users.py +++ b/zerver/management/commands/bankrupt_users.py @@ -21,7 +21,7 @@ class Command(BaseCommand): print("e-mail %s doesn't exist in the system, skipping" % (email,)) continue - do_update_message_flags(user_profile, "add", "read", None, True) + do_update_message_flags(user_profile, "add", "read", None, True, None, None) messages = Message.objects.filter( usermessage__user_profile=user_profile).order_by('-id')[:1] diff --git a/zerver/tests/test_unread.py b/zerver/tests/test_unread.py index 31945f66b9..f5c847c874 100644 --- a/zerver/tests/test_unread.py +++ b/zerver/tests/test_unread.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*-AA from __future__ import absolute_import from zerver.models import ( - get_user_profile_by_email, Recipient, UserMessage, + get_user_profile_by_email, Recipient, UserMessage ) -from zerver.lib.test_helpers import AuthedTestCase +from zerver.lib.test_helpers import AuthedTestCase, tornado_redirected_to_list import ujson class PointerTest(AuthedTestCase): @@ -137,3 +137,102 @@ class UnreadCountTests(AuthedTestCase): for msg in self.get_old_messages(): self.assertEqual(msg['flags'], []) + def test_mark_all_in_stream_read(self): + self.login("hamlet@zulip.com") + user_profile = get_user_profile_by_email("hamlet@zulip.com") + self.subscribe_to_stream(user_profile.email, "test_stream", user_profile.realm) + + message_id = self.send_message("hamlet@zulip.com", "test_stream", Recipient.STREAM, "hello") + unrelated_message_id = self.send_message("hamlet@zulip.com", "Denmark", Recipient.STREAM, "hello") + + events = [] + with tornado_redirected_to_list(events): + result = self.client.post("/json/messages/flags", {"messages": ujson.dumps([]), + "op": "add", + "flag": "read", + "stream_name": "test_stream"}) + + self.assert_json_success(result) + self.assertTrue(len(events) == 1) + + event = events[0]['event'] + expected = dict(operation='add', + messages=[message_id], + flag='read', + type='update_message_flags', + all=False) + + differences = [key for key in expected if expected[key] != event[key]] + self.assertTrue(len(differences) == 0) + + um = list(UserMessage.objects.filter(message=message_id)) + for msg in um: + if msg.user_profile.email == "hamlet@zulip.com": + self.assertTrue(msg.flags.read) + else: + self.assertFalse(msg.flags.read) + + unrelated_messages = list(UserMessage.objects.filter(message=unrelated_message_id)) + for msg in unrelated_messages: + if msg.user_profile.email == "hamlet@zulip.com": + self.assertFalse(msg.flags.read) + + + def test_mark_all_in_invalid_stream_read(self): + self.login("hamlet@zulip.com") + invalid_stream_name = "" + result = self.client.post("/json/messages/flags", {"messages": ujson.dumps([]), + "op": "add", + "flag": "read", + "stream_name": invalid_stream_name}) + self.assert_json_error(result, 'No such stream \'\'') + + def test_mark_all_in_stream_topic_read(self): + self.login("hamlet@zulip.com") + user_profile = get_user_profile_by_email("hamlet@zulip.com") + self.subscribe_to_stream(user_profile.email, "test_stream", user_profile.realm) + + message_id = self.send_message("hamlet@zulip.com", "test_stream", Recipient.STREAM, "hello", "test_topic") + unrelated_message_id = self.send_message("hamlet@zulip.com", "Denmark", Recipient.STREAM, "hello", "Denmark2") + events = [] + with tornado_redirected_to_list(events): + result = self.client.post("/json/messages/flags", {"messages": ujson.dumps([]), + "op": "add", + "flag": "read", + "topic_name": "test_topic", + "stream_name": "test_stream"}) + + self.assert_json_success(result) + self.assertTrue(len(events) == 1) + + event = events[0]['event'] + expected = dict(operation='add', + messages=[message_id], + flag='read', + type='update_message_flags', + all=False) + + differences = [key for key in expected if expected[key] != event[key]] + self.assertTrue(len(differences) == 0) + + um = list(UserMessage.objects.filter(message=message_id)) + for msg in um: + if msg.user_profile.email == "hamlet@zulip.com": + self.assertTrue(msg.flags.read) + + unrelated_messages = list(UserMessage.objects.filter(message=unrelated_message_id)) + for msg in unrelated_messages: + if msg.user_profile.email == "hamlet@zulip.com": + self.assertFalse(msg.flags.read) + + + def test_mark_all_in_invalid_topic_read(self): + self.login("hamlet@zulip.com") + invalid_topic_name = "abc" + result = self.client.post("/json/messages/flags", {"messages": ujson.dumps([]), + "op": "add", + "flag": "read", + "topic_name": invalid_topic_name, + "stream_name": "Denmark"}) + self.assert_json_error(result, 'No such topic \'abc\'') + diff --git a/zerver/views/messages.py b/zerver/views/messages.py index 7a7cd58898..77808a4c0a 100644 --- a/zerver/views/messages.py +++ b/zerver/views/messages.py @@ -603,11 +603,26 @@ def get_old_messages_backend(request, user_profile, @has_request_variables def update_message_flags(request, user_profile, - messages=REQ('messages', validator=check_list(check_int)), - operation=REQ('op'), flag=REQ('flag'), - all=REQ('all', validator=check_bool, default=False)): + messages=REQ('messages', validator=check_list(check_int)), + operation=REQ('op'), flag=REQ('flag'), + all=REQ('all', validator=check_bool, default=False), + stream_name=REQ('stream_name', default=None), + topic_name=REQ('topic_name', default=None)): + request._log_data["extra"] = "[%s %s]" % (operation, flag) - do_update_message_flags(user_profile, operation, flag, messages, all) + stream = None + if stream_name is not None: + stream = get_stream(stream_name, user_profile.realm) + if not stream: + raise JsonableError('No such stream \'%s\'' % (stream_name,)) + if topic_name: + topic_exists = UserMessage.objects.filter(user_profile=user_profile, + message__recipient__type_id=stream.id, + message__recipient__type=Recipient.STREAM, + message__subject__iexact=topic_name).exists() + if not topic_exists: + raise JsonableError('No such topic \'%s\'' % (topic_name,)) + do_update_message_flags(user_profile, operation, flag, messages, all, stream, topic_name) return json_success({'result': 'success', 'messages': messages, 'msg': ''})