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}}
+
+
+
+
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': ''})